সরাসরি পর্বে যান
পর্ব ৩৫ · ৯৩-এর মধ্যে · মডিউল ৫

Versioning আর OpenAPI — চুক্তি লেখা আর docs

API Versioning and OpenAPI — Writing the Contract and Useful Docs

Taught in Bangla, this lesson explains why old app versions cannot be updated, which changes are breaking and which are safe, four places to put a version, /v1 and /v2 routers over one service in Express, retiring a version with Deprecation and Sunset headers and usage logs, OpenAPI as a machine-readable contract generated from Zod, Swagger UI or Scalar docs, linting with Spectral, catching breaks with oasdiff, and typed clients with openapi-typescript.

এই পর্বে যা শিখবেন

  • কোন বদল client ভাঙে আর কোনটি ভাঙে না, একটি পরিষ্কার তালিকায়
  • URL, header, media type আর তারিখ, চার ধরনের versioning-এর সত্যিকারের দাম
  • Express-এ দুই version চালানো আর পুরোনোটিকে মেপে মেপে অবসর দেওয়া
  • OpenAPI 3.1 document-এর গঠন, design-first বনাম code-first, আর Zod থেকে document বানানো
  • CI-তে lint আর breaking change ধরা, typed client তৈরি, আর এমন docs লেখা যা মানুষ সত্যিই ব্যবহার করে

১ · পুরোনো অ্যাপ বদলানো যায় না

বাজারঘরের website আপনি একবার deploy করলে সবার browser পরের reload-এ নতুন কোড পায়। Mobile অ্যাপ এমন নয়। রহিমা চাচি বাজারঘরের Android অ্যাপ আট মাস আগে নামিয়েছেন। Update চালু নেই, ফোনে জায়গাও কম। তাঁর ফোনের কোডটি আপনার হাতের বাইরে, অথচ সেটি প্রতিদিন আপনার server ডাকছে। এক শুক্রবার আপনি ভাবলেন, দামকে পয়সায় রাখলে হিসাব পরিষ্কার হয়। price: 45 বদলে দিলেন price: 4500. Deploy শেষ, test সবুজ। রহিমা চাচির ফোনে এখন মরিচের দাম ৪,৫০০ টাকা।

দর্জির দোকানে মাপের খাতায় লেখা আছে “বুক ৪০”, সব কারিগর জানেন এটি ইঞ্চি। মালিক একদিন নিজে থেকে সেন্টিমিটারে লেখা শুরু করলেন। খাতা দেখতে একই রকম, কলামও একই। কিন্তু পুরোনো কারিগরের হাতে এখন ১০২ ইঞ্চির জামা তৈরি হবে। সমস্যা সংখ্যায় নয়, না-বলা চুক্তি ভাঙায়।

এখানে চুক্তি মানে API-র সবকিছু: URL, method, ঘরের নাম, type, অর্থ, একক, error-এর আকার। পর্ব ২৯-এ আপনি code-এর ভেতরের স্তরগুলো interface দিয়ে আলাদা করেছেন। API হলো সেই একই ধারণা, কিন্তু অন্য পক্ষ আপনার repository-র বাইরে। ভেতরের interface বদলালে compiler সব জায়গা দেখিয়ে দেয়। বাইরের চুক্তি বদলালে কেউ দেখায় না, ব্যবহারকারী শুধু ভুল দাম দেখেন।

Web client নিয়েও একই ঝুঁকি কম মাত্রায় আছে। ব্যবহারকারী খোলা tab রেখে দেন, CDN পুরোনো JS ধরে রাখে, partner দোকান আপনার API দিয়ে নিজের সিস্টেম বানায়। তাই প্রশ্নটি “কীভাবে version দেব” দিয়ে শুরু নয়। প্রশ্নটি হলো: কোন বদল কাকে ভাঙবে?

মূল ধারণা

Server আর client একসাথে বদলায় না। যে মুহূর্তে দুটি আলাদা সময়ে deploy হয়, ঠিক তখনই চুক্তি দরকার। Version নম্বর চুক্তি নয়। চুক্তি হলো সেই লিখিত নিয়ম, যা দুই পক্ষ মেনে চলে।

REST resource, status code আর pagination এই মডিউলের পর্ব ৩২, ৩৩ আর ৩৪-এর বিষয়। এখানে সেগুলো আবার শেখানো হবে না। এই পর্বের প্রশ্ন একটিই: যা বানিয়েছেন, সেটি মানুষকে না ভেঙে কীভাবে বদলাবেন আর কীভাবে লিখে রাখবেন। তাত্ত্বিক ভিত্তি দেখতে চাইলে System Design-এর REST API নকশা পড়ুন।

২ · কোন বদল ভাঙে, কোনটি ভাঙে না

একটি সহজ নিয়ম মনে রাখুন। Response-এ আপনি কথা দেন, request-এ আপনি শর্ত দেন। কথা কমানো ভাঙন, শর্ত বাড়ানোও ভাঙন। উল্টো দিকে কথা বাড়ানো আর শর্ত কমানো সাধারণত নিরাপদ।

বদলফলকেন
Response-এ নতুন ঐচ্ছিক ঘরনিরাপদসহনশীল client উপেক্ষা করে
নতুন endpointনিরাপদপুরোনো client ডাকেই না
Request-এ নতুন ঐচ্ছিক ঘর বা query, default সহনিরাপদনা পাঠালে আগের আচরণ
Response থেকে ঘর মোছা বা নাম বদলানোভাঙেclient খালি মান পড়ে
ঘরের type বদল (45 থেকে {"amount":4500})ভাঙেপুরোনো parser ভুল পড়ে
অর্থ বা একক বদল (টাকা থেকে পয়সা)ভাঙে, আর যন্ত্রে ধরা পড়ে নাtype একই, মান ১০০ গুণ
Request-এ নতুন বাধ্যতামূলক ঘরভাঙেপুরোনো client পাঠায় না
যাচাই কড়া করা (max ১০০ থেকে ২০)ভাঙেআগের বৈধ request এখন 422
Status code বদল (200 থেকে 201)ভাঙেকেউ === 200 দেখছে
Response-এর নতুন enum মানঝুঁকিপূর্ণপুরোনো switch-এ ঘর নেই
ঘরের ক্রম, error-এর লেখা, response timeচুক্তিতে নেই, তবু ঝুঁকিকেউ না কেউ এর ওপর নির্ভর করে

শেষ সারিটির একটি নাম আছে, Hyrum's Law. Google-এর প্রকৌশলী Hyrum Wright-এর নামে পরিচিত এই পর্যবেক্ষণ বলে, ব্যবহারকারী যথেষ্ট হলে আপনার সিস্টেমের প্রতিটি দৃশ্যমান আচরণের ওপর কেউ না কেউ নির্ভর করবে, আপনি কথা দিন বা না দিন। তাই তালিকাটি পূর্ণ গ্যারান্টি নয়। এটি ঝুঁকির মানচিত্র।

নিচের ধাপগুলোতে একই পুরোনো অ্যাপের সামনে দুই ধরনের বদল দেখুন। দুই রায়ই oasdiff নামের টুলের সত্যিকারের run থেকে নেওয়া, যা অংশ ৮-এ চালাব।

GET /v1/products/7আজকের চুক্তি
{"id":7,"name":"কাঁচা মরিচ","price":45}

পুরোনো অ্যাপের স্ক্রিন: মরিচ, ৪৫ টাকা
নিরাপদ বদলএকটি ঘর যোগ
{"id":7,"name":"কাঁচা মরিচ","price":45,"unit":"কেজি"}

পুরোনো অ্যাপ unit চেনে না, উপেক্ষা করে:
মরিচ, ৪৫ টাকা
oasdiffexit code 0
info [response-optional-property-added]
  in API GET /products/{id}
  added the optional property `unit`

breaking: No breaking changes to report
ভাঙা বদলprice-এর type বদল
{"id":7,"name":"কাঁচা মরিচ","price":{"amount":4500,"currency":"BDT"}}

পুরোনো অ্যাপের স্ক্রিন:
মরিচ, [object Object] টাকা
oasdiffexit code 1
error [response-property-type-changed]
  in API GET /products/{id}
  the `price` response's property `type`
  changed from `integer` to `object`
  for status `200`
সঠিক পথপাশাপাশি দুই চুক্তি
/v1/products/7  → "price":45                অপরিবর্তিত
/v2/products/7  → "price":{"amount":4500,...}
  1. ধাপ ১ — আজকের চুক্তি। পুরোনো অ্যাপ price পড়ে ৪৫ টাকা দেখায়। এই আচরণই এখন চুক্তি।
  2. ধাপ ২ — একটি ঘর যোগ। unit এলো, কিন্তু আগের তিনটি ঘর একই আছে। পুরোনো অ্যাপ যেটি চেনে না, সেটি না পড়লে স্ক্রিন বদলায় না। একেই বলে additive change.
  3. ধাপ ৩ — যন্ত্রের রায়: নিরাপদ। oasdiff শুধু একটি info দিল, exit code 0. Build এগিয়ে যেতে পারে।
  4. ধাপ ৪ — type বদল। price এখন object. কোনো crash নেই, অথচ JavaScript object-কে লেখায় বদলে স্ক্রিনে [object Object] ছাপল। নীরব ভাঙনই সবচেয়ে বিপজ্জনক।
  5. ধাপ ৫ — যন্ত্রের রায়: ভাঙা। --fail-on ERR দিয়ে চালালে exit code 1, তাই pipeline থেমে যায়। ভুলটি মানুষের ফোনে পৌঁছানোর আগেই ধরা পড়ে।
  6. ধাপ ৬ — সঠিক পথ। ভাঙা বদল সত্যিই দরকার হলে পুরোনো চুক্তি রেখে নতুন একটি পাশে খুলুন। /v1 আগের মতোই চলে, নতুন অ্যাপ /v2 ডাকে।

যোগ করে বদলানো আর সহনশীল পাঠক

ঘর যোগ নিরাপদ হয় একটি শর্তে: client-কেও নিয়ম মানতে হবে। এই নিয়মের নাম tolerant reader, অর্থাৎ যা লাগে শুধু তা পড়ুন, বাকিটা উপেক্ষা করুন, আর যা পড়লেন তা যাচাই করুন। উল্টো দিকে “কঠোর client” চেনা ঘরের বাইরে কিছু দেখলেই থেমে যায়। নিচের কোডে তিন রকম উত্তর দুই client-কে দেওয়া হয়েছে। চালিয়ে দেখুন।

// আগের উত্তর, ঘর যোগের পরের উত্তর, আর ঘরের অর্থ বদলের পরের উত্তর
const before = '{"id":7,"name":"Morich","price":45}';
const added  = '{"id":7,"name":"Morich","price":45,"unit":"kg"}';
const broken = '{"id":7,"name":"Morich","price":{"amount":4500}}';

// কঠোর client: চেনা ঘরের বাইরে কিছু দেখলেই থেমে যায়
function strictClient(text) {
  const body = JSON.parse(text);
  for (const key of Object.keys(body)) {
    if (!['id', 'name', 'price'].includes(key)) {
      throw new Error(`unknown field: ${key}`);
    }
  }
  return `${body.name}: ${body.price} taka`;
}

// সহনশীল client: যা লাগে শুধু তা পড়ে, আর সেটুকু যাচাই করে
function tolerantClient(text) {
  const { name, price } = JSON.parse(text);
  if (typeof price !== 'number') throw new Error('price is not a number');
  return `${name}: ${price} taka`;
}

for (const [label, text] of [['before', before], ['added ', added], ['broken', broken]]) {
  for (const [who, read] of [['strict  ', strictClient], ['tolerant', tolerantClient]]) {
    try {
      console.log(label, who, 'OK  ', read(text));
    } catch (e) {
      console.log(label, who, 'FAIL', e.message);
    }
  }
}

আউটপুটের দ্বিতীয় জোড়াটি দেখুন: added strict FAIL unknown field: unit. একটি নিরীহ ঘর যোগেই কঠোর client ভেঙেছে, সহনশীল client ঠিক আছে। তৃতীয় জোড়াটি আরও শিক্ষণীয়। broken strict OK Morich: [object Object] taka বলছে, কঠোর client ঘরের নাম দেখেছে, type দেখেনি, তাই ভুল লেখা হাসিমুখে ছেপেছে। আর broken tolerant FAIL price is not a number দেখায়, সহনশীল মানে অসতর্ক নয়। যে ঘরটি সে ব্যবহার করে, সেটি সে ঠিকই যাচাই করে থামল। পর্ব ২৭-এর z.object ঠিক এই আচরণ করে: অচেনা ঘর নীরবে বাদ দেয়, আর strict object অচেনা ঘরে error দেয়। Response পড়ার সময় তাই সাধারণ z.object ভালো, request নেওয়ার সময় যেখানে অজানা ঘর আটকাতে চান সেখানে strict.

Response-এ নতুন enum মান যোগ করা type-এর দিক থেকে যোগ, কিন্তু আচরণের দিক থেকে ঝুঁকি। অর্ডারের status-এ "on_hold" এলে পুরোনো অ্যাপের switch কিছুই দেখাতে পারে না। শুরু থেকেই client-এ একটি “অচেনা অবস্থা” শাখা রাখুন, আর docs-এ লিখুন যে নতুন মান আসতে পারে।

৩ · Version কোথায় লিখবেন: চার ধরন

ভাঙা বদল এড়ানো না গেলে client-কে বলতে হবে সে কোন চুক্তি চায়। এই “কোথায় বলবে” প্রশ্নের চারটি প্রচলিত উত্তর আছে। কোনোটি সব জায়গায় সেরা নয়, আর এই নিয়ে সমাজে মতভেদ আছে।

ধরনদেখতেসুবিধাদাম
URL/v2/products/7চোখে দেখা যায়, log আর cache-এ আলাদা, browser-এ পরীক্ষা সহজএকই resource-এর দুই ঠিকানা; ছোট বদলেও পুরো নম্বর বাড়ে
Custom headerApi-Version: 2URL পরিষ্কার থাকেLink-এ পরীক্ষা করা যায় না; cache-এ Vary লাগে; ভুলে গেলে default কী?
Media typeAccept: application/vnd.bazarghar.v2+jsonHTTP-র content negotiation মানে; resource-প্রতি version সম্ভবলেখা ও পরীক্ষা কঠিন; টুল আর নতুনদের কাছে অপরিচিত; এটিও Vary চায়
তারিখApi-Version: 2026-11-01প্রতিটি ভাঙা বদল ছোট; client একটি তারিখে আটকে থাকে, ধীরে এগোয়পুরোনো রূপে ফেরানোর রূপান্তর-শিকল চালাতে হয়; ছোট দলের জন্য ভারী

তারিখ-ভিত্তিক versioning-এর পরিচিত প্রকাশ্য বিবরণ আছে Stripe-এর ২০১৭ সালের প্রকৌশল-লেখায়। সেখানে তারা জানায়, প্রতিটি account একটি তারিখে আটকে থাকে, আর ভেতরে রূপান্তরের ধাপ দিয়ে পুরোনো চেহারা বানানো হয়। বড় দলের জন্য এটি শক্তিশালী, কিন্তু সেই যন্ত্রপাতি বানানো ও রক্ষা করা নিজেই একটি প্রকল্প।

বাজারঘরের সিদ্ধান্ত: URL versioning, শুধু major নম্বর (/v1, /v2). কারণ দলটি ছোট, client-রা বাইরের, আর log-এ এক নজরে বোঝা জরুরি কে কোন version ডাকছে। /v1.3 লিখবেন না। Minor বদল additive, তাই নতুন ঠিকানা লাগে না। Semver-এর MINOR আর PATCH থাকবে document-এর info.version-এ।

একটি ফাঁদও জেনে রাখুন। Header-ভিত্তিক version-এ একই URL দুই রকম উত্তর দেয়। CDN যদি শুধু URL দেখে cache করে, v1-এর client v2-র উত্তর পেতে পারে। তাই response-এ Vary: Api-Version দিতে হয়। URL versioning-এ এই সমস্যা জন্মায়ই না।

৪ · Express-এ /v1 আর /v2, service একটিই

সবচেয়ে সাধারণ ভুল হলো পুরো ফোল্ডার কপি করা: routes-v2/, services-v2/. এতে একটি bug ঠিক করতে দুই জায়গায় হাত দিতে হয়, আর কিছুদিন পরে দুই কপির নিয়ম আলাদা হয়ে যায়। ছাড়ের হিসাব v1-এ এক, v2-তে আরেক। মনে রাখুন, version আলাদা হয় চুক্তিতে, ব্যবসার নিয়মে নয়।

একই রান্নাঘর, দুই রকম প্লেট। রেস্তোরাঁর বাবুর্চি একটিই ভুনা রাঁধেন। ওয়েটার কাউকে সাদা ভাতের সঙ্গে দেন, কাউকে পার্সেলের বাক্সে। রান্না বদলাতে হলে এক জায়গায় বদলালেই হয়। Router হলো ওয়েটার, service হলো রান্নাঘর, আর presenter হলো পরিবেশনের ধরন।

দুই চুক্তি, এক service

Two routers present one service in two shapes

/v1 আর /v2 router একই service ডাকছে পুরোনো অ্যাপ ফোনে আটকে আছে নতুন অ্যাপ সদ্য update হওয়া /v1 router toV1: price 45 Deprecation Sunset header /v2 router toV2: price amount 4500 currency BDT productService একটিই নিয়ম, দাম পয়সায় Database pricePoisha: 4500
ছবি ৩৫.১ — ডেটা এক জায়গায়, চেহারা দুই রকম। v1 বাদ দিতে হলে শুধু বাঁ দিকের বাক্সটি মুছবেন।

আগে Node-এর নিজের http module-এ ধারণাটি দেখি। এখানে কোনো framework নেই, তাই সব কিছু চোখের সামনে।

const http = require('node:http');

// একটিমাত্র service: দাম সব সময় পয়সায়
const service = {
  get: (id) => (id === 7 ? { id, name: 'Morich', poisha: 4500, stock: 12 } : null),
};

// প্রতিটি version শুধু উত্তরের চেহারা ঠিক করে
const present = {
  v1: (p) => ({ id: p.id, name: p.name, price: p.poisha / 100 }),
  v2: (p) => ({ id: p.id, name: p.name,
    price: { amount: p.poisha, currency: 'BDT' }, inStock: p.stock > 0 }),
};

const server = http.createServer((req, res) => {
  const m = req.url.match(/^\/(v1|v2)\/products\/(\d+)$/);
  const product = m && service.get(Number(m[2]));
  if (!product) {
    res.writeHead(404);
    return res.end();
  }
  res.writeHead(200, { 'content-type': 'application/json' });
  res.end(JSON.stringify(present[m[1]](product)));
});

server.listen(0, '127.0.0.1', async () => {
  const base = `http://127.0.0.1:${server.address().port}`;
  for (const path of ['/v1/products/7', '/v2/products/7', '/v3/products/7']) {
    const res = await fetch(base + path);
    console.log(path, res.status, await res.text());
  }
  server.close();
});

/v1/products/7 200 {"id":7,"name":"Morich","price":45} আর /v2/products/7 200 {"id":7,"name":"Morich","price":{"amount":4500,"currency":"BDT"},"inStock":true} এসেছে একই service.get থেকে। পার্থক্য শুধু present-এ। /v3/products/7 404 মনে করিয়ে দেয়, যে version নেই তার জন্য অনুমান করবেন না, সরাসরি 404 দিন। আর stock ঘরটি v1-এর উত্তরে নেই, কারণ presenter নিজে থেকে ঠিক করে কী বেরোবে। এতে database-এর নতুন কলাম নিজে থেকে API-তে ফাঁস হয় না।

এবার আসল Express. পর্ব ২৫-এর express.Router একটি factory function থেকে বানালে দুই version একই route পায়, শুধু presenter আলাদা। নিচের কোড সত্যিকারের project থেকে নেওয়া, ছোট করে দেখানো হলো। সত্যিকারের run. লেখার সময়ের সংস্করণ: Node 24.15.0, Express 5.3.0, Zod 4.6.7, TypeScript 5.9.3, tsx 4.23.15.

const toV1 = (r: Row) => ({ id: r.id, name: r.name, price: r.pricePoisha / 100 });
const toV2 = (r: Row) => ({
  id: r.id, name: r.name,
  price: { amount: r.pricePoisha, currency: 'BDT' as const },
  inStock: r.stock > 0,
});

function productRouter(present: (r: Row) => unknown) {
  const router = Router();
  router.get('/products/:id', async (req, res) => {
    const { id } = ProductParams.parse(req.params);
    const row = await svc.get(id);
    if (!row) {
      res.status(404).type('application/problem+json').json({
        type: 'about:blank', title: 'Not Found', status: 404,
        detail: `পণ্য ${id} পাওয়া যায়নি`,
      });
      return;
    }
    res.json(present(row));
  });
  return router;
}

app.use('/v1', deprecated, productRouter(toV1));
app.use('/v2', productRouter(toV2));

deprecated middleware পরের অংশে দেখবেন। শুধু /v1-এর সামনে বসানোয় v2 এর খরচ দেয় না। দুই version-এর সত্যিকারের উত্তর:

$ curl -si -H 'X-Client-Id: android-3.2.0' http://127.0.0.1:3935/v1/products/7
HTTP/1.1 200 OK
X-Powered-By: Express
Deprecation: @1793491200
Sunset: Sat, 01 May 2027 00:00:00 GMT
Link: <https://bazarghar.example/migrate-v2>; rel="deprecation"; type="text/html"
Content-Type: application/json; charset=utf-8
Content-Length: 83
…
{"id":7,"name":"কাঁচা মরিচ ২৫০ গ্রাম","price":45}

$ curl -si http://127.0.0.1:3935/v2/products/7
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
…
{"id":7,"name":"কাঁচা মরিচ ২৫০ গ্রাম","price":{"amount":4500,"currency":"BDT"},"inStock":true}

দুই version-এ একটি গোপন ঝুঁকি আছে, আর এটি আমাদের নিজের কোডেই ধরা পড়েছে। v1-এর চুক্তিতে price পূর্ণসংখ্যা। কিন্তু দাম ১২০ টাকা ৫০ পয়সা হলে toV1 দেয় 120.5. সত্যিকারের run-এ Zod বলল: 12050 -> 120.5 price: Invalid input: expected int, received number. পুরোনো দাম পূর্ণ টাকায় ছিল, তাই service-এর নতুন সূক্ষ্মতা পুরোনো চুক্তিতে ধরছে না। Presenter-এই সিদ্ধান্ত নিতে হবে: গোল করা, নাকি v1-এ এমন দাম এড়ানো। এটিই contract test-এর কাজ, অংশ ৮-এ আবার আসছে।

৫ · পুরোনো version-কে অবসর দেওয়া

দুই version চিরকাল চালানো যায় না। প্রতিটি bug fix, প্রতিটি নতুন নিয়ম দুই presenter-এর সঙ্গে মেলাতে হয়। ৩টি version আর ২৪টি endpoint মানে ৭২টি জোড়া পরীক্ষা করা। আবার হঠাৎ বন্ধ করলে রহিমা চাচির অ্যাপ মরে যায়। মাঝের পথটির নাম deprecation: আগে থেকে জানানো, মাপা, তারপর বন্ধ করা।

HTTP-তে এর জন্য দুটি মানসম্মত header আছে, আর দুটির তারিখের ধরন আলাদা:

Headerমানদণ্ডমানঅর্থ
DeprecationRFC 9745 (মার্চ ২০২৫, Standards Track)@1793491200, Unix সেকেন্ডএই তারিখ থেকে পুরোনো বলে গণ্য। আচরণ বদলায় না।
SunsetRFC 8594 (মে ২০১৯, Informational)Sat, 01 May 2027 00:00:00 GMT, HTTP-dateএই সময়ের পরে সাড়া নাও দিতে পারে। একটি ইঙ্গিত, গ্যারান্টি নয়।
LinkRFC 9745, RFC 8594rel="deprecation" বা rel="sunset"নির্দেশিকার ঠিকানা।

RFC 9745 বলে, Sunset-এর সময় Deprecation-এর সময়ের আগে হতে পারবে না। আর দুই RFC-ই পরিষ্কার: এই header জানানোর জন্য, বন্ধ করার যন্ত্র নয়। Middleware-টি ছোট:

const DEPRECATED_AT = Date.UTC(2026, 10, 1);      // 2026-11-01
const SUNSET_AT = Date.UTC(2027, 4, 1);           // 2027-05-01

export const deprecated: RequestHandler = (req, res, next) => {
  res.set('Deprecation', `@${DEPRECATED_AT / 1000}`);
  res.set('Sunset', new Date(SUNSET_AT).toUTCString());
  res.set('Link', '<https://bazarghar.example/migrate-v2>; rel="deprecation"; type="text/html"');
  res.on('finish', () => {
    console.log(JSON.stringify({
      msg: 'deprecated_call',
      path: req.path,
      status: res.statusCode,
      client: req.get('x-client-id') ?? 'unknown',
      ua: req.get('user-agent') ?? 'unknown',
    }));
  });
  next();
};

Header পাঠালেই কাজ শেষ নয়। Client-কে সেটি পড়তেও হবে, নইলে কেউ জানবে না। আপনার নিজের অ্যাপে একটি ছোট পাঠক রাখুন, যা error log-এ একবার সতর্ক করে। নিচে নকল ঘড়ি দিয়ে সেটি চলছে।

const http = require('node:http');
const server = http.createServer((req, res) => {
  res.writeHead(200, {
    'content-type': 'application/json',
    Deprecation: '@1793491200',
    Sunset: 'Sat, 01 May 2027 00:00:00 GMT',
    Link: '<https://bazarghar.example/migrate-v2>; rel="deprecation"',
  });
  res.end('{"id":7}');
});
const warned = new Set();                       // একই path-এ একবারই সতর্কতা
const DAY = 24 * 60 * 60 * 1000;

function checkRetirement(path, headers, now) {
  const dep = headers.get('deprecation');
  if (!dep || warned.has(path)) return;
  warned.add(path);
  const deprecatedAt = Number(dep.slice(1)) * 1000;   // '@' বাদ, সেকেন্ড থেকে ms
  const sunsetAt = Date.parse(headers.get('sunset'));
  console.log('deprecated on', new Date(deprecatedAt).toISOString().slice(0, 10));
  console.log('sunset on    ', new Date(sunsetAt).toISOString().slice(0, 10));
  console.log('days left    ', Math.round((sunsetAt - now) / DAY));
  console.log('guide        ', headers.get('link'));
}

server.listen(0, '127.0.0.1', async () => {
  const url = `http://127.0.0.1:${server.address().port}/v1/products/7`;
  const today = Date.UTC(2027, 0, 30);          // নকল ঘড়ি: ৩০ জানুয়ারি ২০২৭
  for (let i = 0; i < 3; i++) {
    const res = await fetch(url);
    checkRetirement('/v1/products/7', res.headers, today);
    console.log('call', i + 1, res.status, await res.text());
  }
  server.close();
});

deprecated on 2026-11-01 এসেছে @1793491200 থেকে, sunset on 2027-05-01 এসেছে HTTP-date থেকে। একই কাজে দুই পার্স, কারণ দুই RFC দুই সময়ে লেখা। days left 91 মানে ৩০ জানুয়ারি থেকে ৯১ দিন বাকি। তিনটি call হলো, কিন্তু সতর্কতা ছাপা হলো একবারই, warned Set-এর জন্য। আর তিনটি call-ই 200 পেল: deprecation আচরণ বদলায় না। Set না থাকলে প্রতিটি request-এ একই log উঠত, আর মানুষ সেটি দেখা ছেড়ে দিতেন।

কে এখনও ডাকছে, মেপে দেখুন

তারিখ ঠিক করা সহজ, সিদ্ধান্ত নেওয়া কঠিন। উত্তর আসে পর্ব ৩১-এর structured logging থেকে। উপরের middleware প্রতিটি পুরোনো call-এ একটি JSON লাইন লেখে। নিজের অ্যাপে X-Client-Id header বসিয়ে রাখলে এক লাইনের হিসাবেই নাম বেরিয়ে আসে। সত্যিকারের run, সাতটি পরীক্ষার call:

$ node count.mjs out/server.log
   4 android-3.2.0
   1 ios-2.1.0
   1 partner-dokan
   1 unknown

unknown সারিটিই সবচেয়ে দামি। এটি বলে, এমন কেউ ডাকছে যাকে আপনি চেনেন না, তাই ইমেইলও করতে পারবেন না। User-Agent, API key বা rate limit-এর হিসাব মিলিয়ে তাকে খুঁজতে হয়।

কত সপ্তাহে ট্রাফিক নামবে: একটি হিসাব

ধরুন (অনুমিত সংখ্যা) প্রথম সপ্তাহে v1-এ 100000 call, আর প্রতি সপ্তাহে ২৫% কমে। কত সপ্তাহে 1000-এর নিচে নামবে?

calls(n) = 100000 × 0.75^n
100000 × 0.75^n < 1000
0.75^n < 0.01
n > ln(0.01) / ln(0.75) = 16.008
সপ্তাহ 16: 1002   (এখনও বেশি)
সপ্তাহ 17:  752   (নিচে নেমেছে)

উত্তর ১৭ সপ্তাহ। গুণিতক হারে কমা ট্রাফিকে শেষের অংশটুকুই সবচেয়ে ধীর: প্রথম ৪ সপ্তাহে ৬৮% কমে যায়, কিন্তু ১%-এ নামতে মোট চারগুণ সময় লাগে। পুরো অবসরটি নিচের ধাপে দেখুন। সংখ্যাগুলো এই একই অনুমিত সূত্রের।

/v1-এর অবস্থা
সময়
call/সপ্তাহ
header
সাধারণ উত্তর
  1. ধাপ ১ — ঘোষণা। v2 চালু, নতুন অ্যাপ বাজারে। v1-এ header শুরু, docs-এ migration গাইড, চেনা client-দের ইমেইল। সপ্তাহে 100000 call. আচরণ বদলায়নি।
  2. ধাপ ২ — মাপা। সংখ্যা 10011-এ নেমেছে, অর্থাৎ প্রায় ৯০% call সরে গেছে। এবার log থেকে বাকিদের নাম বের করে আলাদা করে যোগাযোগ।
  3. ধাপ ৩ — ছোট বন্ধ। 752-এ নামার পরে দিনের নির্দিষ্ট অল্প সময়ে 410 ফেরানো শুরু। একে বলে brownout. যে client নোটিশ পড়েনি, সে এবার টের পায়, অথচ সারাদিনের জন্য ভাঙে না।
  4. ধাপ ৪ — শেষ সপ্তাহ। 75 call. প্রায় সবাই সরেছেন। এখন বাধা দূর করতে অ্যাপে “update করুন” স্ক্রিন, যা পুরোনো version-এর 410 দেখে দেখানো যায়।
  5. ধাপ ৫ — Sunset. ১ মে ২০২৭ থেকে সব উত্তর 410 Gone, সঙ্গে নির্দেশিকার link. 404-এর চেয়ে 410 বেশি পরিষ্কার: “ছিল, এখন নেই”।
  6. ধাপ ৬ — পরিষ্কার। কিছুদিন পরে toV1, deprecated আর /v1 মাউন্ট মুছে ফেলুন। এক জায়গায় presenter থাকায় মোছা ছোট কাজ।

মোবাইল অ্যাপের বাস্তবতা: ব্যবহারকারীকে জোর করে update করাতে চাইলে সেই কোড আগের অ্যাপে থাকতে হবে। তাই প্রথম version থেকেই একটি সহজ “ন্যূনতম version” পরীক্ষা রাখুন। Server থেকে minSupportedVersion পড়ে অ্যাপ নিজেই update-এর বার্তা দেখাবে। দ্বিতীয় অভ্যাস: X-Client-Id শুরু থেকেই পাঠান।

৬ · OpenAPI: চুক্তিকে যন্ত্রে পড়ার মতো করে লেখা

এতক্ষণ চুক্তি ছিল মানুষের মাথায় আর একটি wiki পাতায়। OpenAPI হলো HTTP API-র চুক্তি লেখার একটি মানসম্মত ভাষা, YAML বা JSON-এ। একবার লেখা থাকলে মানুষও পড়তে পারে, যন্ত্রও: docs, client, lint, পরীক্ষা, সব একই ফাইল থেকে আসে।

২০১০-এর দশকের শুরুতে Tony Tam-এর Swagger প্রকল্প থেকে এর জন্ম। ২০১৫ সালের নভেম্বরে SmartBear আর কয়েকটি প্রতিষ্ঠান Linux Foundation-এর অধীনে OpenAPI Initiative গড়ে, আর specification-এর নাম হয় OpenAPI. “Swagger” নামটি এখন মূলত টুলে বেঁচে আছে। ৩.০ আসে জুলাই ২০১৭-তে, ৩.১ ফেব্রুয়ারি ২০২১-এ, যা JSON Schema-র সঙ্গে পুরো মিলে যায়। ৩.২.০ আসে ১৯ সেপ্টেম্বর ২০২৫-এ। লেখার সময়ে spec.openapis.org-এর সর্বশেষটি ৩.২.১ (১০ সেপ্টেম্বর ২০২৬).

এই পর্বের document 3.1.1, কারণ zod-openapi 6.0.2-এর README শুধু 3.1.0 আর 3.1.1 সমর্থনের কথা বলে। নতুন spec বেরোলেই সব টুল তা ধরতে পারে না, তাই version বাছুন আপনার টুলের সবচেয়ে দুর্বল সদস্য দেখে। আমাদের run-এ Spectral, Redocly, oasdiff আর openapi-typescript openapi: 3.2.0 লেখা ফাইল পড়তে আপত্তি করেনি, তবে আমরা 3.2-এর নতুন কোনো সুবিধা পরীক্ষা করিনি।

Document-এর শীর্ষে চারটি অংশ মনে রাখলেই চলে:

info আর servers

নাম, version, যোগাযোগ, আর API কোন ঠিকানায় থাকে।

পরিচয়পত্র

paths

প্রতিটি URL আর method: কী নেয়, কী ফেরায়। প্রতিটি কাজের নাম operationId.

মেনু

components

বারবার লাগা schema আর auth-এর ধরন। $ref দিয়ে ডাকা হয়।

মশলার তাক

examples

Schema বলে আকার, example বলে সত্যিকারের চেহারা। মানুষ আগে example পড়েন।

নমুনা প্লেট

বাজারঘরের সত্যিকারের ফাইল থেকে একটি অংশ। পুরো ফাইলটি ১৯১ লাইন, আর Redocly ও Spectral-এ কোনো error ছাড়াই পাস করেছে।

openapi: 3.1.1
info:
  title: বাজারঘর API
  version: 2.0.0
servers:
  - url: https://api.bazarghar.example/v2
security: []
paths:
  /products/{id}:
    get:
      operationId: getProduct
      summary: একটি পণ্য
      tags:
        - products
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
            exclusiveMinimum: 0
      responses:
        "200":
          description: পাওয়া গেছে
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductV2"
              examples:
                chili:
                  summary: মজুত আছে এমন পণ্য
                  value:
                    id: 7
                    name: কাঁচা মরিচ ২৫০ গ্রাম
                    price:
                      amount: 4500
                      currency: BDT
                    inStock: true
        "404":
          description: এই id-র পণ্য নেই
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

লক্ষ্য করুন, 404-এর জন্যও আলাদা schema আছে। Error চুক্তির অংশ। application/problem+json হলো পর্ব ৩৩-এর problem details ধরন। আর security: [] মানে এই API-র কাজগুলোতে সাধারণভাবে login লাগে না; /me/orders নিজের security দিয়ে bearer token চায়।

আগে নকশা, নাকি আগে কোড

Design-firstCode-first
শুরুআগে YAML লেখা, পরে কোডআগে কোড আর schema, পরে document তৈরি
ভালো যখনকয়েকটি দল, partner বা আগেভাগে আলোচনা দরকারএকই repository-র ছোট দল
ঝুঁকিকোড ধীরে ধীরে document থেকে সরে যায়নকশা নিয়ে ভাবার আগেই চুক্তি হয়ে যায়
পাহারাContract test লাগবেইLint আর oasdiff দিয়ে নকশা-পর্যালোচনা

মূল কথা, সত্যের উৎস একটিই রাখুন। দুই জায়গায় হাতে লেখা চুক্তি একসময় আলাদা হবেই। বাজারঘরের দল Express আর Zod আগেই ব্যবহার করছে, তাই তারা code-first. কিন্তু document-এর বদল pull request-এ পড়ে দেখা হয়, যাতে নকশা-পর্যালোচনাও থাকে।

৭ · Zod থেকে document, Express থেকে docs

আপনার কাছে পর্ব ২৭-এর Zod schema আগে থেকেই আছে। সেগুলোই request যাচাই করে, TypeScript type দেয়। Document-ও তা থেকে এলে তিনটি জিনিস এক উৎসে থাকে। Zod 4-এর জন্য দুটি পরিচিত package আছে, আর লেখার সময়ে দুটিরই peerDependencies-এ zod ^4.0.0: zod-openapi (6.0.2) আর @asteasolutions/zod-to-openapi (9.1.0). আমরা প্রথমটি চালিয়েছি, দ্বিতীয়টি চালাইনি। Zod 4-এর .meta()-তে id দিলে schema components-এ যায়।

export const Money = z.object({
  amount: z.number().int().nonnegative()
    .meta({ description: 'পয়সায় দাম', examples: [4500] }),
  currency: z.literal('BDT'),
}).meta({ id: 'Money' });

export const ProductV2 = z.object({
  id: z.number().int().positive().meta({ examples: [7] }),
  name: z.string().min(1).max(120).meta({ examples: ['কাঁচা মরিচ ২৫০ গ্রাম'] }),
  price: Money,
  inStock: z.boolean().meta({ examples: [true] }),
}).meta({ id: 'ProductV2' });
import { createDocument } from 'zod-openapi';

export const doc = createDocument({
  openapi: '3.1.1',
  info: { title: 'বাজারঘর API', version: '2.0.0' },
  paths: {
    '/products/{id}': {
      get: {
        operationId: 'getProduct',
        requestParams: { path: ProductParams },
        responses: {
          '200': {
            description: 'পাওয়া গেছে',
            content: { 'application/json': { schema: ProductV2 } },
          },
          '404': problem('এই id-র পণ্য নেই'),
        },
      },
    },
  },
});

সত্যিকারের run-এ npx tsx src/openapi.ts ছাপল wrote out/v1.openapi.yaml, out/v2.openapi.yaml. তৈরি ফাইলে দুটি জিনিস চোখে পড়ল, যা তুলে ধরা দরকার।

  • Response schema-য় additionalProperties: false বসেছে। কাগজে এটি বলছে “এর বাইরে আর কোনো ঘর আসবে না”, অথচ আমরা চাই client সহনশীল হোক আর নতুন ঘর যোগ করা নিরাপদ থাকুক। পরীক্ষায় দেখা গেল, response-এ z.object দেয় additionalProperties: false, আর z.looseObject দেয় additionalProperties: {}. Request body-র z.object-এ এই লাইনটি আসেনি। Document থেকে কোনো কঠোর client যাচাই বানালে এই লাইনটি আগে পড়ে নিন।
  • maximum: 9007199254740991 এসেছে। এটি .int()-এর নিরাপদ সীমা, ক্ষতি নেই, তবে পড়তে কিছুটা অস্বস্তিকর।

এবার মানুষের জন্য পাতা। পর্ব ২৬-এর মতো এগুলোও সাধারণ middleware.

import swaggerUi from 'swagger-ui-express';
import { apiReference } from '@scalar/express-api-reference';

const v2Doc = buildDoc('v2');
app.get('/openapi.json', (_req, res) => { res.json(v2Doc); });
app.use('/docs', swaggerUi.serve, swaggerUi.setup(v2Doc));
app.use('/reference', apiReference({ url: '/openapi.json' }));
$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://127.0.0.1:3935/docs/
200 text/html; charset=utf-8
$ curl -s -o /dev/null -w "%{http_code} %{content_type}\n" http://127.0.0.1:3935/reference
200 text/html; charset=utf-8
$ curl -s http://127.0.0.1:3935/docs/ | grep -o 'src="[^"]*"'
src="./swagger-ui-bundle.js"
src="./swagger-ui-standalone-preset.js"
src="./swagger-ui-init.js"
$ curl -s http://127.0.0.1:3935/reference | grep -o 'https://[^"]*'
https://cdn.jsdelivr.net/npm/@scalar/api-reference/esm.js'

দুটি পাতাই Edge browser-এ খুলে দেখা হয়েছে। Swagger UI দেখাল বাজারঘর API, 2.0.0, OAS 3.1, Authorize বোতাম আর operation-এর তালিকা। Scalar-ও একই document দেখাল। শেষের আউটপুটটি একটি সত্যিকারের পার্থক্য দেখায়: Swagger UI-র ফাইল আপনার server থেকেই আসে, আর Scalar-এর এই package-এর পাতা একটি CDN থেকে JavaScript আনে। অভ্যন্তরীণ নেটওয়ার্কে বা কড়া CSP-তে এই পার্থক্য গুরুত্বপূর্ণ।

Production-এ docs খোলা রাখবেন কি না, সেটি সচেতন সিদ্ধান্ত। Public API-র docs খোলা থাকাই স্বাভাবিক। Admin route থাকলে সেই document আলাদা করুন বা login-এর পেছনে রাখুন। Document গোপন রাখা নিরাপত্তা নয়, প্রতিটি endpoint-এ আসল যাচাই লাগবেই। বিস্তারিত Cybersecurity-র API security পর্বে।

৮ · যন্ত্রকে পাহারায় বসানো: lint আর ভাঙন ধরা

লিখিত চুক্তির সবচেয়ে বড় লাভ, যন্ত্র সেটি পড়তে পারে। Linter দেখে document নিজে ভালো কি না। Diff টুল দেখে দুই document-এর মধ্যে কী ভেঙেছে। ইচ্ছা করে একটি খারাপ খসড়া লিখলাম, একটি $ref-এ বানান ভুল (Prodcut), {id} ঘোষণা নেই, example-এর type ভুল। Spectral 6.17.0-এর ruleset spectral:oas, সঙ্গে আমাদের নিজের একটি নিয়ম: 4xx-এর উত্তর application/problem+json হতে হবে।

extends: ["spectral:oas"]
rules:
  operation-description: error
  bazarghar-error-has-problem-json:
    description: 4xx response must use application/problem+json
    message: "{{path}} must describe application/problem+json"
    severity: error
    given: "$.paths[*][*].responses[?(@property >= '400' && @property < '500')].content"
    then:
      field: application/problem+json
      function: truthy
$ npx spectral lint out/draft.openapi.yaml

~/bazarghar/out/draft.openapi.yaml
  1:1   warning  oas3-api-servers           OpenAPI "servers" must be present and non-empty array.
  2:6   warning  info-contact               Info object must have "contact" object.
  2:6   warning  info-description           Info "description" must be present and non-empty string.
  7:9     error  operation-description      Operation "description" must be present and non-empty string.
  7:9   warning  operation-operationId      Operation must have "operationId".
  7:9   warning  operation-tags             Operation must have non-empty "tags" array.
  7:9     error  path-params                Operation must define parameter "{id}" as expected by path "/products/{id}".
 15:23    error  invalid-ref                '#/components/schemas/Prodcut' does not exist
 18:19    error  bazarghar-error-has-problem-json  … must describe application/problem+json
 24:13  warning  oas3-unused-component      Potentially unused component has been detected.
 28:42    error  oas3-valid-schema-example  "example" property type must be integer

✖ 11 problems (5 errors, 6 warnings, 0 infos, 0 hints)

Spectral বানান ভুলের জন্য Product-কে “অব্যবহৃত” বলেও ধরল: একটি ভুল থেকে দুটি বার্তা। Redocly CLI 2.72.1 একই ফাইলে ৪টি error আর ৪টি warning দিল, আর দৃষ্টিভঙ্গি একটু আলাদা। তার একটি error হলো security-defined: Every operation should have security defined on it or on the root level. আমাদের প্রথম ভালো document-ও এখানে আটকেছিল। পণ্য দেখতে login লাগে না, তাই root-এ security: [] লিখে সিদ্ধান্তটি প্রকাশ্য করা হলো। এই নিয়মটি শিক্ষণীয়: টুল আপনাকে login দিতে বলে না, শুধু “ভুলে গেছেন, না ইচ্ছা করে?” প্রশ্নটি তোলে।

$ npx redocly lint out/v1.openapi.yaml out/v2.openapi.yaml
…
Woohoo! Your API descriptions are valid. 🎉

দুই document মেলানো

Lint একটি ফাইল দেখে, তাই সে জানে না গতকাল কী ছিল। ভাঙা বদল ধরতে দরকার দুই ফাইল মেলানো। oasdiff (1.35.0) একটি Go প্রোগ্রাম, GitHub release থেকে নামিয়ে checksum মিলিয়ে চালালাম। npm-এ oasdiff নামে যা আছে তা এই টুল নয়, সেটি 0.0.1-security নামের একটি ফাঁকা নিরাপত্তা-স্থানধারক। ভিন্ন তিনটি বদলের সত্যিকারের ফল:

$ oasdiff breaking out/v1.openapi.yaml out/v1-add.yaml --fail-on ERR
No breaking changes to report, but the specs are different.
Run 'oasdiff diff' to see structural differences.
exit=0

$ oasdiff breaking out/v1.openapi.yaml out/v1-rename.yaml --fail-on ERR
2 changes: 2 error, 0 warning, 0 info
error  [response-required-property-removed] at out/v1-rename.yaml
  in API GET /products
    removed the required property `items/name` from the response with the `200` status
…
exit=1

$ oasdiff breaking out/v1.openapi.yaml out/v1-param.yaml --fail-on ERR
1 changes: 1 error, 0 warning, 0 info
error  [new-required-request-parameter] at out/v1-param.yaml
  in API GET /products
    added the new required `query` request parameter `shop`
exit=1

একটি ছোট কিন্তু দামি তথ্য: --fail-on ERR ছাড়া আমাদের run-এ ৬টি error থাকার পরেও exit code ছিল 0. CI-তে flag না দিলে পাহারাদার চিৎকার করবে, কিন্তু দরজা খোলা থাকবে। পর্ব ০৫-এ দেখা npm script-এর মতো এটিকেও pipeline-এর ধাপ বানান, পর্ব ৮১-তে CI আসবে। ধারণাটি পরিষ্কার করতে এবার একই কাজের একটি খুব ছোট শিক্ষামূলক মডেল চালান। এটি oasdiff নয়, শুধু যুক্তিটি।

// দুই schema মেলানোর শিক্ষামূলক মডেল। side: 'response' বা 'request'
function diff(a, b, side) {
  const out = [];
  const req = (s, k) => s.required.includes(k);
  for (const k of Object.keys(a.properties)) {
    const was = a.properties[k], now = b.properties[k];
    if (!now) out.push(`BREAKING removed ${k}`);
    else if (was.type !== now.type) out.push(`BREAKING type ${k}: ${was.type} -> ${now.type}`);
    else if (side === 'response' && req(a, k) && !req(b, k)) out.push(`BREAKING ${k} no longer always sent`);
    else if (side === 'request' && !req(a, k) && req(b, k)) out.push(`BREAKING ${k} now required`);
  }
  for (const k of Object.keys(b.properties)) {
    if (a.properties[k]) continue;
    if (side === 'request' && req(b, k)) out.push(`BREAKING new required ${k}`);
    else out.push(`ok added ${k}`);
  }
  return out.length ? out : ['safe'];
}

const str = { type: 'string' }, int = { type: 'integer' };
const v1 = { properties: { id: int, name: str, price: int }, required: ['id', 'name', 'price'] };

const cases = {
  'add optional field':  { properties: { ...v1.properties, unit: str }, required: v1.required },
  'rename name -> title': { properties: { id: int, title: str, price: int }, required: ['id', 'title', 'price'] },
  'price becomes string': { properties: { ...v1.properties, price: str }, required: v1.required },
  'name now optional':   { properties: v1.properties, required: ['id', 'price'] },
};
for (const [label, v2] of Object.entries(cases)) {
  console.log(label);
  console.log('  response:', diff(v1, v2, 'response').join('; '));
  console.log('  request: ', diff(v1, v2, 'request').join('; '));
}

শেষ জোড়াটিই আসল শিক্ষা। name now optional করলে response: BREAKING name no longer always sent, কিন্তু request: safe. একই বদল, দিক উল্টো হলে ফল উল্টো। Response-এ আপনি কথা কমালেন, request-এ শর্ত কমালেন, ঠিক অংশ ২-এর নিয়ম। add optional field দুই দিকেই ok. rename name -> title-এ response-এ একটি ভাঙন, request-এ দুটি, কারণ পুরোনো client title পাঠায় না। মডেলটি সরল: এটি nested object, enum বা $ref দেখে না, আর অর্থের বদল কেউই দেখে না। সেখানেই আসল টুল আর মানুষের পর্যালোচনা লাগে।

কোনো diff টুল অর্থ বোঝে না। price: 45 থেকে price: 4500-এ type একই integer, তাই রায় আসবে “নিরাপদ”। অথচ অংশ ১-এর ঘটনাটি ঠিক এটিই। একক বদলালে ঘরের নামও বদলান (pricePoisha), তাহলে ভাঙনটি যন্ত্রে দেখা যাবে।

Contract test, সংক্ষেপে: Lint আর diff শুধু কাগজ দেখে। কোড সেই কাগজ মানছে কি না দেখে contract test. সহজ রূপ: integration test-এ সত্যিকারের response নিয়ে একই Zod schema দিয়ে safeParse করা। অংশ ৪-এর 120.5 ভুল ঠিক এভাবেই ধরা পড়েছিল। বড় রূপে client নিজের প্রত্যাশা লিখে দেয় আর server সেটি পরীক্ষা করে, যাকে বলে consumer-driven contract. Supertest দিয়ে এই test লেখা আসবে পর্ব ৭৫-এ। তত্ত্ব আছে Software Engineering-এর interface আর contract পর্বে।

৯ · Typed client আর এমন docs, যা মানুষ পড়ে

Backend আর frontend আলাদা repository-তে থাকলে ভাগ করা type (পর্ব ০৬) সরাসরি import করা যায় না। OpenAPI সেখানে সেতু। openapi-typescript document থেকে TypeScript type বানায়, আর openapi-fetch সেই type দিয়ে একটি পাতলা client দেয়। এর জন্য চলার সময়ে client-কে document পড়তে হয় না।

$ npx openapi-typescript out/v2.openapi.yaml -o client/bazarghar.d.ts
✨ openapi-typescript 7.13.0
🚀 out/v2.openapi.yaml → client/bazarghar.d.ts [24.6ms]

লক্ষ্য করুন, লেখার সময়ে openapi-typescript 7.13.0-এর peer হলো typescript ^5.x. তাই এই project-এ TypeScript 5.9.3 রাখা হয়েছে; 6.0 দিয়ে npm ERESOLVE দিয়েছে। পুরোনো অ্যাপের মতো ভুল কোড লিখলে compiler সঙ্গে সঙ্গে ধরে:

const api = createClient<paths>({ baseUrl: 'http://127.0.0.1:3935/v2' });
const { data } = await api.GET('/products/{id}', { params: { path: { id: '7' } } });
const taka: number = data!.price;
console.log(taka.toFixed(2));
await api.GET('/product/{id}');
$ npx tsc --noEmit
client/old.ts(5,70): error TS2322: Type 'string' is not assignable to type 'number'.
client/old.ts(6,7): error TS2322: Type '{ amount: number; currency: "BDT"; }' is not assignable to type 'number'.
client/old.ts(8,11): error TS2554: Expected 2 arguments, but got 1.

আর ঠিক কোডটি চলন্ত server-এর বিরুদ্ধে সত্যিকারের ফল দিল। data আর error আলাদা বলে 404-এর detail-ও type-সহ আসে:

$ npx tsx client/app.ts
200 4500 undefined
404 পণ্য 99 পাওয়া যায়নি

পুরো পথটি এখন এক সুতোয় বাঁধা। একটি schema থেকে শুরু করে শেষ পর্যন্ত যান:

  1. Zodschemas.tsউৎস
  2. Document.yamlচুক্তি
  3. পাহারাlint, diffCI
  4. Docs/docsমানুষ
  5. Client.d.tsকোড
schemas.tsএক জায়গায় লেখা
Money = z.object({
  amount: z.number().int().nonnegative(),
  currency: z.literal('BDT'),
}).meta({ id: 'Money' })
v2.openapi.yamlতৈরি হওয়া ফাইল
Money:
  type: object
  properties:
    amount:
      type: integer
      minimum: 0
      …
    currency:
      type: string
      const: BDT
Lintএকটি document ভালো কি না
$ npx spectral lint out/v2.openapi.yaml
No results with a severity of 'error' found!
Diffv1 থেকে v2: প্রত্যাশিত ভাঙন
$ oasdiff breaking v1 v2 --fail-on ERR
6 changes: 6 error, 0 warning, 0 info
exit=1
GET /docs/Swagger UI
বাজারঘর API  2.0.0  OAS 3.1
[Authorize]
GET  /products
GET  /products/{id}
GET  /me/orders
bazarghar.d.ts197 লাইনের ফাইলের অংশ
Money: {
  /** @description পয়সায় দাম */
  amount: number;
  /** @constant */
  currency: "BDT";
};
  1. ধাপ ১ — উৎস। নিয়ম লেখা হলো Zod-এ। এই schema-ই চলার সময়ে response যাচাই করতে পারে। id: 'Money' বলে দিল, এটি পুনর্ব্যবহারযোগ্য।
  2. ধাপ ২ — Document. createDocument সেটিকে components.schemas.Money-তে বসাল। .int() হলো integer, .nonnegative() হলো minimum: 0, .literal হলো const.
  3. ধাপ ৩ — Lint. Spectral কোনো error পেল না। মানে document নিজে ঠিকঠাক গড়া। এটি বলে না যে API ভালো নকশার।
  4. ধাপ ৪ — Diff. v1 থেকে v2-তে ৬টি error. এগুলো ইচ্ছাকৃত, তাই v2 আলাদা ঠিকানায় আছে। একই version-এ এই রায় এলে build থামত।
  5. ধাপ ৫ — মানুষের জন্য docs. Swagger UI একই ফাইল পড়ে পাতা সাজাল। Authorize বোতামটি bearerAuth থেকে এসেছে।
  6. ধাপ ৬ — কোডের জন্য client. openapi-typescript type বানাল। Backend amount-এর নাম বদলালে পরের build-এ frontend-এর compile ভাঙবে, মানুষের ফোনে নয়।

যে docs মানুষ সত্যিই ব্যবহার করে

তৈরি পাতা মানেই ভালো docs নয়। Schema বলে কী সম্ভব, docs বলে কীভাবে কাজ হবে। পাঁচটি জিনিস যোগ করুন:

আরও গভীরে: RFC 9745-এর Deprecation মান একটি Structured Field Date (RFC 9651), তাই সামনে @. Sunset পুরোনো HTTP-date রেখেছে, যা RFC 9745 নিজেই “ঐতিহাসিক কারণ” বলে উল্লেখ করে। OpenAPI-তে ৩.১ থেকে Schema Object-এ একক example ঘরের জায়গায় JSON Schema-র examples তালিকা পছন্দনীয়, আমাদের schema তাই সেটি লিখেছে। একটি operation পুরোনো বলতে document-এ deprecated: true লিখলে Swagger UI সেটি কেটে দেখায়, তবে তা HTTP header-এর বিকল্প নয়, কারণ চলমান client document পড়ে না।

পরীক্ষা ও ইন্টারভিউতে:

  • Breaking change কী? যে বদলে আগের বৈধ client আর ঠিকভাবে চলে না: ঘর মোছা, type বদল, নতুন বাধ্যতামূলক ইনপুট।
  • URL না header version? URL সহজ ও cache-বান্ধব, header-এ URL পরিষ্কার কিন্তু Vary লাগে। প্রসঙ্গ দেখে বলুন।
  • Deprecation আর Sunset-এর পার্থক্য? প্রথমটি “ব্যবহার না করাই ভালো”, দ্বিতীয়টি “এই সময়ের পরে নাও চলতে পারে”।
  • OpenAPI আর Swagger কি এক? OpenAPI হলো specification, Swagger UI ও অন্যান্য হলো টুল। নামটি আগে Swagger specification ছিল।
  • Design-first কখন? কাজ শুরুর আগে কয়েকটি দল বা partner-কে একমত হতে হলে।

১০ · AI-এর যুগে API-র চুক্তি

AI এক মিনিটে YAML আর route লিখে দেয়। সমস্যা হলো, চুক্তি ভাঙলে কোনো লাল দাগ পড়ে না। কোড চলে, test সবুজ, ভাঙে অন্য কারও ফোন। তাই এই বিষয়ে AI-র কাজ যাচাই করা বাড়তি জরুরি।

AI-এর ভুলকীভাবে ধরবেন
“শুধু নাম গুছিয়ে দিলাম” বলে ঘরের নাম বদলানোoasdiff breaking --fail-on ERR
Deprecation: true, বা ISO তারিখে Sunset লেখাRFC 9745 আর RFC 8594-এর ধরন মেলান
পুরোনো Swagger 2.0-এর definitions বা পুরোনো Zod APIআপনার package-এর version-এর README পড়ুন
নেই এমন package বা ভুল নাম, যেমন npm i oasdiffnpm view, আর official install পাতা
Docs-এ public দেখানো route-এ auth ভুলে যাওয়াRedocly-র security-defined, আর 401 পরীক্ষা
Example ও schema না মেলা, বানানো responseLint, তারপর আসল server-এ curl

AI-কে কাজ দেওয়ার সময় version, সীমা আর যাচাইয়ের দাবি একসাথে দিন। শেষ দুই লাইন AI-কে নিজের কাজের ঝুঁকি বলতে বাধ্য করে।

আমার Express 5 আর Zod 4 project-এ v1 API চালু আছে, আর পুরোনো মোবাইল অ্যাপ সেটি ডাকে। নিচের OpenAPI 3.1 ফাইল দিলাম। [এখানে v1.openapi.yaml পেস্ট করুন] আমি products-এ price-কে টাকা থেকে পয়সায় নিতে চাই। ১. এই বদল breaking কি না বলো, আর কোন client কীভাবে ভাঙবে। ২. v1 না ভেঙে v2 যোগ করার ন্যূনতম diff দাও: নতুন presenter আর router, service কপি করবে না। ৩. কোনো নতুন package যোগ করলে কারণ বলো, আর সেটি npm-এ আছে কি না আমি কীভাবে দেখব। ৪. আমি কোন oasdiff আর curl কমান্ড চালিয়ে তোমার কাজ যাচাই করব, আর কী আউটপুট আসা উচিত। ৫. তুমি কোন বিষয়ে নিশ্চিত নও, আলাদা করে লেখো।

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১URL-এ /v1 লেখা কি REST-এর নীতির বিরুদ্ধে? তবু এত API এটি কেন ব্যবহার করে?

নীতির দিক থেকে আপত্তিটি শক্ত। Roy Fielding-এর ২০০০ সালের গবেষণাপত্রে REST-এ একটি resource একটি পরিচয় পায়, আর তার representation বদলাতে পারে। পণ্য ৭ তো একই পণ্য, শুধু তার JSON-এর চেহারা বদলেছে। তাহলে /v1/products/7 আর /v2/products/7 দুটি ঠিকানা এক জিনিসকে দুই নামে ডাকছে। এই যুক্তি থেকে অনেকে media type-এ version রাখার কথা বলেন, কারণ HTTP-র Accept ঠিক এই কাজের জন্যই আছে।

বাস্তবে সিদ্ধান্ত হয় অন্য মাপে। URL version চোখে দেখা যায়। Browser-এ পেস্ট করলে চলে, log-এ grep করা যায়, CDN আলাদা করে cache করে, reverse proxy দুই version-কে দুই service-এ পাঠাতে পারে। নতুন ডেভেলপার পাঁচ সেকেন্ডে বোঝেন। পুরোনো version-এর জন্য আলাদা server রাখতে হলেও URL দেখেই ভাগ করা যায়, যার জন্য অতিরিক্ত কোনো কোড লেখার দরকার হয় না। Media type-এ এই সব কাজের জন্য header পড়তে হয়, Vary সামলাতে হয়, আর SDK ঠিকমতো header না পাঠালে নীরবে default version পাওয়া যায়।

আরেকটি সূক্ষ্ম কথা আছে। URL version অনেকটা বড় ভাঙনের জন্য বানানো, যেখানে পুরো API-র চরিত্র বদলায়। প্রতিটি ছোট বদলে নম্বর বাড়ানোর জন্য নয়। অনেক দল তাই বছরের পর বছর /v1-এই থাকে, additive বদল করে চলে। এই চর্চা দেখায়, নম্বরটি আসল সমাধান নয়, শেষ আশ্রয়।

মূল উপলব্ধি: নীতি আপনাকে প্রশ্ন করতে শেখায়, কিন্তু সিদ্ধান্ত নেয় আপনার client, টুল আর দলের বাস্তবতা। ছোট দলের জন্য সহজে দেখা যায় এমন সমাধান প্রায়ই সেই সমাধানের চেয়ে ভালো, যা তত্ত্বে নিখুঁত কিন্তু ভুল করা সহজ।

প্র ০২তারিখ-ভিত্তিক versioning ছোট দলের জন্য কি ঠিক? Stripe-এর মডেল থেকে কী শেখার আছে?

Stripe ২০১৭ সালে “APIs as infrastructure” নামের লেখায় তাদের পদ্ধতির বিবরণ দেয়। সেখানে দাবি, ২০১১ থেকে তারা প্রতিটি version-এর সামঞ্জস্য রেখেছে। প্রতিটি account প্রথম call-এর দিনের version-এ আটকে যায়। ভাঙা বদল এলে একটি নতুন তারিখ হয়, আর পুরোনো আচরণ মূল কোডে না থেকে আলাদা “version change module”-এ থাকে। তাদের বিবরণ অনুযায়ী, response আগে সর্বশেষ চেহারায় তৈরি হয়, তারপর module-গুলো উল্টো ক্রমে চলে client-এর তারিখ পর্যন্ত ফিরিয়ে আনে।

এই নকশার সৌন্দর্য হলো, ব্যবসার কোড শুধু বর্তমানকে জানে। আমাদের toV1 আর toV2-র পেছনের চিন্তাও একই: ভেতরে এক সত্য, বাইরে কয়েকটি চেহারা। পার্থক্য হলো মাত্রায়। Stripe-এ শত শত ছোট ধাপ থাকতে পারে, আর সেগুলোর পরীক্ষা, ক্রম ও ডকুমেন্টেশন রক্ষার জন্য আলাদা যন্ত্রপাতি লাগে। Stripe নিজেই লেখে, এটি একটি আপস, আর তারা প্রথমবারেই নকশা ঠিক করার চেষ্টা করে যাতে এই যন্ত্র কম লাগে।

বাজারঘরের মতো ছোট দলের জন্য পুরো মডেল ভারী। কয়েকটি বাইরের client আর দুটি version-এ একটি presenter ফাংশনই যথেষ্ট। তবে দুটি শিক্ষা নেওয়া যায়। এক, পুরোনো আচরণকে মূল ব্যবসায়িক কোডে if (version === 1) হিসেবে ছড়াবেন না, সীমানায় রাখুন। দুই, client-কে নিজের গতিতে এগোতে দিন, কারণ সবাই একদিনে বদলাতে পারে না। তৃতীয় একটি শিক্ষাও আছে: যে দিনই নতুন চেহারা যোগ হয়, সেই দিনই তার রূপান্তরের নিয়মও লিখে ফেলুন, পরে মনে রাখার ওপর ছেড়ে দেবেন না।

মূল উপলব্ধি: বড় কোম্পানির সমাধান নকল করবেন না, তার পেছনের নীতিটি নিন। নীতিটি হলো, পুরোনো আচরণ বাঁচিয়ে রাখার খরচ এক জায়গায় আটকে রাখা।

প্র ০৩GraphQL বলে তার version লাগে না, আর protobuf-এর আছে কড়া নিয়ম। এরা REST-কে কী শেখায়?

GraphQL-এর দর্শন হলো একটি বিবর্তনশীল schema. Client ঠিক যে ঘরগুলো চায়, সেগুলোই লিখে চায়, তাই নতুন ঘর যোগে কেউ প্রভাবিত হয় না। ঘর পুরোনো হলে @deprecated দিয়ে চিহ্নিত করা হয়। একটি বড় সুবিধা: server জানে কোন client কোন ঘর চাইছে, তাই “কেউ এখনও price পড়ছে কি না” প্রায় ঘর-ধরে মাপা যায়। REST-এ পুরো response যায়, তাই শুধু endpoint পর্যন্ত মাপা যায়। পর্ব ৩৬-এ GraphQL আসবে।

তবে “version নেই” মানে “ভাঙন নেই” নয়। GraphQL-এও ঘর মুছলে পুরোনো query ব্যর্থ হয়। সমস্যা একই, শুধু সমাধান additive দিকে ঝোঁকা।

gRPC-র protobuf আরও সোজা। প্রতিটি ঘরের একটি নম্বর আছে, আর wire-এ নাম নয়, নম্বর যায়। মুছে ফেলা ঘরের নম্বর আবার ব্যবহার করা উচিত নয়, তাই reserved করে রাখা হয়। অচেনা ঘর parser এড়িয়ে যায়। অর্থাৎ tolerant reader এখানে ফরম্যাটের ভেতরেই গাঁথা, কারও ভালো অভ্যাসের ওপর নির্ভর নয়। পুরোনো ফোন আর নতুন server বছরের পর বছর কথা বলতে পারে এই নিয়মের জন্যই।

REST আর JSON-এ এমন কোনো যন্ত্র গাঁথা নেই। সেই ফাঁক পূরণ করতেই OpenAPI, Spectral আর oasdiff-এর মতো টুল। ১৯৮০ সালে Jon Postel TCP-র নথিতে যে নীতি লেখেন, “পাঠানোয় সংযত, গ্রহণে উদার”, সেটি এখানেও প্রযোজ্য, তবে অতিরিক্ত উদারতাই আবার Hyrum's Law-এর রাস্তা খোলে, এই সমালোচনাও মনে রাখতে হবে।

মূল উপলব্ধি: বিবর্তনের নিয়ম প্রযুক্তিতে গাঁথা থাকলে মানুষের শৃঙ্খলা কম লাগে। JSON-এ সেই শৃঙ্খলা আপনাকে টুল আর CI দিয়ে আনতে হবে।

প্র ০৪Document কোড থেকে আসা উচিত, নাকি কোড document থেকে? “চুক্তি আগে” ধারণার ইতিহাস কী বলে?

চুক্তি আগে লেখার ধারণা নতুন নয়। ১৯৯০-এর দশকে CORBA-র IDL, ২০০০-এর দশকে SOAP-এর WSDL, দুটিই যন্ত্রে পড়ার মতো interface লিখত, আর তা থেকে client তৈরি হতো। সমস্যা ছিল ভার: XML-এর পাহাড়, জটিল টুল, ভিন্ন vendor-এর মধ্যে অমিল। REST আর JSON জনপ্রিয় হলো অনেকটা সেই ভার ফেলে দিয়ে, আর সঙ্গে হারাল লিখিত চুক্তি। মানুষ wiki পাতায় API লিখতে লাগলেন, যা দ্রুত পুরোনো হতো। Swagger সেই হারানো জিনিসটি হালকা রূপে ফেরাল।

Design-first-এর পক্ষে বড় যুক্তি মানসিক। কোড লেখার আগে YAML-এ আলোচনা করলে নাম আর আকার বদলানো সস্তা, frontend আর mobile দল mock দিয়ে সমান্তরালে কাজ শুরু করতে পারে। Partner-দের কাছে প্রতিশ্রুতি দিতে হলে এটিই স্বাভাবিক। দুর্বলতা হলো সরে যাওয়া: YAML এক কথা বলে, কোড আরেক কথা করে, আর কেউ টের পায় না যদি না contract test থাকে।

Code-first-এর পক্ষে যুক্তি ব্যবহারিক। Zod schema request যাচাই করছেই, তাই document প্রায় নিজে থেকে সত্য থাকে, যদিও response যাচাই না করলে সেখানে ফাঁক থাকে। দুর্বলতা হলো, চুক্তি কোডের পার্শ্বফল হয়ে যায়। কেউ এক লাইন বদলালে API বদলে গেল, অথচ pull request-এ সেটি চোখে পড়ল না। তৈরি হওয়া YAML-কে repository-তে রেখে diff দেখলে এই ঝুঁকি অনেকটা কমে।

দুটি শিবিরের মাঝে একটি ব্যবহারিক মিলন আছে: উৎস যাই হোক, চূড়ান্ত document-কে একটি পর্যালোচনাযোগ্য জিনিস হিসেবে দেখা, আর CI-তে সেটি পাহারা দেওয়া।

মূল উপলব্ধি: প্রশ্নটি “কোনটি আগে” নয়। প্রশ্ন হলো, সত্যের উৎস একটি কি না, আর চুক্তির বদল কারও চোখে পড়ে কি না।

প্র ০৫Sunset-এর দিন এসে গেল, কিন্তু সবচেয়ে বড় partner এখনও সরেনি। আপনি কী করবেন?

এটি প্রযুক্তির চেয়ে সম্পর্ক আর ঝুঁকির প্রশ্ন। RFC 8594 নিজেই Sunset-কে ইঙ্গিত বলে, গ্যারান্টি নয়, তাই তারিখ পিছিয়ে দেওয়া চুক্তিভঙ্গ নয়। আবার বারবার পেছালে নোটিশের মূল্য কমে: পরের বার কেউ বিশ্বাস করবে না, আর দেরি যে পুরস্কৃত হয়, সবাই সেটি শিখে যাবে।

প্রথম কাজ সংখ্যা দেখা। ওই partner মোট আয়ের কত অংশ? v1 চালিয়ে যাওয়ার খরচ কত? কোনো নিরাপত্তা ত্রুটি কি v1-এ বাধ্য করে বন্ধ করতে? নিরাপত্তার কারণ হলে আলোচনা ভিন্ন, সেখানে দ্রুত বন্ধ করাই দায়িত্ব। আর শুধু রক্ষণাবেক্ষণের খরচ হলে সময় কেনা যায়, তবে তা লিখিতভাবে একটি নতুন, শেষ তারিখে, হয়তো নির্দিষ্ট API key-এর জন্য, বাকি সবার জন্য নয়।

দ্বিতীয় কাজ, পথ সহজ করা। Migration গাইড আছে? কোনো ঘরের সরাসরি ম্যাপিং আছে? প্রায়ই সমস্যা ইচ্ছার অভাব নয়, partner-এর দলের সময়। একজন প্রকৌশলীকে কয়েক দিনের জন্য সাহায্যে দেওয়া, দুই version চালানোর চেয়ে সস্তা হতে পারে।

তৃতীয় কাজ, brownout. নির্ধারিত ছোট বন্ধে যে দল আগে কথা শোনেনি, সে এবার শোনে, কারণ alert বাজে। আগে থেকে সময় জানিয়ে দিলে এটি ন্যায্য। ৪১০-এর উত্তরে নির্দেশিকার link থাকলে তাদের engineer ঠিক জায়গায় পৌঁছান।

আর ভবিষ্যতের জন্য, চুক্তিতেই লিখুন আপনার deprecation নীতি: ন্যূনতম কত মাস নোটিশ, কোথায় জানানো হবে। আগে থেকে জানা নিয়ম কঠিন সিদ্ধান্তকে ব্যক্তিগত ঝগড়া থেকে বাঁচায়।

মূল উপলব্ধি: API অবসর একটি প্রকৌশল প্রক্রিয়া আর একটি প্রতিশ্রুতি রক্ষার গল্প, দুটিই। Header জানায়, log মাপে, কিন্তু শেষ সিদ্ধান্ত নেয় মানুষ, লেখা নীতি সামনে রেখে।

অনুশীলন

নিজে চেষ্টা করে তারপর উত্তর মেলান।

  1. হিসাব করুন: একটি পুরোনো version-এ এই সপ্তাহে 80000 call. প্রতি সপ্তাহে ২০% কমে। (ক) কত সপ্তাহ পরে সংখ্যা 2000-এর নিচে নামবে? (খ) ৩টি version, ২৪টি endpoint, প্রতিটির ৪টি test হলে মোট কয়টি test চালাতে হবে?

    (ক) 80000 × 0.8^n < 2000 থেকে 0.8^n < 0.025, অর্থাৎ n > ln(0.025) / ln(0.8) = 16.53. পরীক্ষা: সপ্তাহ ১৬-তে 2252, সপ্তাহ ১৭-তে 1801. তাই ১৭ সপ্তাহ।

    (খ) ৩ × ২৪ × ৪ = ২৮৮টি test. একটি version কমলে ১৯২টি। এটিই পুরোনো version বাঁচিয়ে রাখার একটি মাপা যায় এমন দাম।

  2. আউটপুট অনুমান করুন: Presenter হলো const toV1 = (r) => ({ price: r.pricePoisha / 100 });. চুক্তির schema price: z.number().int(). pricePoisha 4500 আর 12050, দুটির জন্য safeParse কী বলবে?

    সত্যিকারের run:

    4500 -> 45 matches contract
    12050 -> 120.5 price: Invalid input: expected int, received number

    প্রথমটি পূর্ণসংখ্যা, দ্বিতীয়টি নয়। Presenter চুপচাপ চুক্তি ভেঙেছে, অথচ test-এ যদি শুধু ৪৫ টাকার পণ্য থাকে, কেউ দেখবে না। তাই contract test-এ পয়সাসহ দাম রাখুন।

  3. নকশা বিচার করুন: সহকর্মী এক pull request-এ এই পাঁচটি বদল চান: (১) /v1-এ response-এ discount ঐচ্ছিক ঘর যোগ, (২) name বদলে title, (৩) POST /v1/orders-এ phone বাধ্যতামূলক, (৪) limit-এর সর্বোচ্চ ১০০ থেকে ৫০, (৫) পণ্যের weight গ্রাম থেকে কেজিতে। কোনগুলো অনুমোদন করবেন, বাকিগুলোর কী করবেন?

    (১) অনুমোদন। Additive. (২) ভাঙে। title যোগ করুন, দুটি ঘরই কিছুদিন পাঠান, আর name সরান v2-তে। (৩) ভাঙে। v1-এ ঐচ্ছিক রাখুন, v2-তে বাধ্যতামূলক করুন। (৪) ভাঙে। limit=80 আগে বৈধ ছিল। Server চাইলে ৫০টির বেশি না দিয়ে পরের পাতা দিতে পারে, যদি চুক্তিতে তার সুযোগ থাকে। (৫) সবচেয়ে বিপজ্জনক। Type একই, তাই oasdiff ধরবে না। নতুন ঘর weightKg যোগ করুন। আর একটি PR-এ পাঁচটি মেশাবেন না, যাতে পর্যালোচনা ও rollback সহজ থাকে।

  4. কমান্ড চালান: নিজের project-এ পুরোনো document base.yaml আর নতুন rev.yaml আছে। এমন একটি কমান্ড লিখুন, যা ভাঙা বদল পেলে CI থামাবে। আর ব্যাখ্যা করুন কেন শুধু oasdiff breaking base.yaml rev.yaml যথেষ্ট নয়।
    oasdiff breaking base.yaml rev.yaml --fail-on ERR

    এই পর্বের run-এ flag ছাড়া ৬টি error ছাপার পরেও exit code ছিল 0, আর flag দিলে 1. CI কেবল exit code দেখে, লেখা পড়ে না। base.yaml নিন main শাখার document থেকে, rev.yaml নিন এই শাখার তৈরি document থেকে। যদি ভাঙনটি ইচ্ছাকৃত হয়, তবে একই version-এ না রেখে নতুন version-এ নিন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি পুরোনো API version অবসরের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. নতুন version চালু করে migration গাইড লেখা
  2. পুরোনো version-এ Deprecation আর Sunset header শুরু করা
  3. Log দেখে কে এখনও ডাকছে তা মাপা আর তাদের জানানো
  4. নির্ধারিত অল্প সময়ের জন্য brownout চালানো
  5. Sunset-এর দিন থেকে 410 Gone ফেরানো
  6. পুরোনো route আর presenter-এর কোড মুছে ফেলা

একটি public JSON API-তে কোন বদলটি সাধারণত non-breaking?

  • Response-এর name ঘরের নাম বদলে title করা
  • Response-এ একটি নতুন ঐচ্ছিক ঘর যোগ করা
  • Request-এ একটি নতুন বাধ্যতামূলক ঘর যোগ করা
  • price-এর type সংখ্যা থেকে object করা

একটি client response-এ অচেনা ঘর পেলে সেটি উপেক্ষা করে, আর নিজের দরকারি ঘরটুকু যাচাই করে পড়ে। এই নীতির নাম কী?

  • Hyrum's Law
  • Idempotency
  • Tolerant reader
  • Brownout

RFC 9745 অনুযায়ী Deprecation response header-এর সঠিক উদাহরণ কোনটি?

  • Deprecation: true
  • Deprecation: 2026-11-01
  • Deprecation: Sun, 01 Nov 2026 00:00:00 GMT
  • Deprecation: @1793491200

দুটি API version চালানোর সময় ব্যবসার নিয়ম কোথায় রাখা সবচেয়ে ভালো?

  • একটি service-এ, আর প্রতিটি version-এর আলাদা presenter বা router-এ শুধু চেহারা
  • প্রতিটি version-এর জন্য service-এর আলাদা কপিতে
  • প্রতিটি ফাংশনের ভেতরে if (version === 1) শর্তে
  • প্রতিটি client অ্যাপের নিজের কোডে

price ঘরটি integer-ই আছে, কিন্তু মান টাকা থেকে পয়সায় বদলে গেছে। oasdiff-এর মতো টুল এটি ধরবে কি?

  • হ্যাঁ, type-বদল হিসেবে error দেবে
  • হ্যাঁ, কারণ সে আসল response পড়ে
  • না, কারণ document-এর আকার একই; অর্থের বদল ধরা পড়ে না
  • না, তবে Spectral সব সময় ধরবে

OpenAPI document-এর components অংশের প্রধান কাজ কী?

  • প্রতিটি URL আর HTTP method লেখা
  • পুনর্ব্যবহারযোগ্য schema, security scheme ইত্যাদি রাখা, যা $ref দিয়ে ডাকা হয়
  • Server-এর চলমান কোড রাখা
  • API-র নাম আর version লেখা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. API একটি চুক্তি। পুরোনো client আপনি বদলাতে পারেন না, তাই আগে additive পথ খুঁজুন।
  2. Response-এ কথা কমানো আর request-এ শর্ত বাড়ানো breaking; অর্থের বদল যন্ত্রে ধরা পড়ে না।
  3. Version আলাদা হয় চেহারায়, service-এ নয়। বাজারঘর URL-এ শুধু major নম্বর রাখে।
  4. Deprecation আর Sunset জানায়, log মাপে, আর brownout শেষ সতর্কতা দেয়।
  5. OpenAPI থেকে docs, lint, diff আর typed client আসে; সত্যের উৎস একটি রাখুন, আর --fail-on ERR ভুলবেন না।

পরের পর্বে: REST-এ server ঠিক করে কী ফিরবে। পর্ব ৩৬-এ client নিজেই ঠিক করবে কোন ঘর চায়: GraphQL-এর schema, resolver আর তার নিজের N+1 সমস্যা।

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

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান