সরাসরি পর্বে যান
পর্ব ০৮ · ৬৬-এর মধ্যে · মডিউল ২

REST API নকশা — resource, status code, pagination, versioning আর idempotency

REST API Design — Resources, Status Codes, Pagination, Versioning & Idempotency

Taught in Bangla, this lesson designs a bus-ticket booking API to teach REST: resources and URIs, safe and idempotent methods, choosing status codes with a uniform RFC 9457 error format, offset versus cursor pagination with a live SQL query plan, versioning and deprecation, idempotency keys for safe POST retries, ETag and If-Match for concurrent edits, long-running jobs, batch endpoints, OpenAPI and security.

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

  • Resource, URI আর method দিয়ে অনুমানযোগ্য API সাজানো — বাস-টিকিট বুকিংয়ের পূর্ণ উদাহরণে
  • প্রতিটি পরিস্থিতিতে ঠিক status code, আর এক ছাঁচের ত্রুটি-বার্তা
  • Offset বনাম cursor pagination — লাইভ SQL-এ query plan আর দ্বিগুণ-হারানো সারি
  • পুরোনো app না ভেঙে API বদলানো: versioning আর অবসরের পরিকল্পনা
  • Idempotency key আর ETag দিয়ে নিরাপদ retry ও সম্পাদনা; সাথে দীর্ঘ কাজ, batch, OpenAPI আর নিরাপত্তা

১ · REST কী — resource, representation আর Fielding-এর নীতি

পর্ব ০৬-এ নকশার ধাপ ৩-এ API-কে বলেছিলাম রেস্তোরাঁর মেনু, আর লিখেছিলাম দুই-চারটি মূল endpoint. এই পর্বে মেনুটি ভালোভাবে লেখার নিয়ম: কোন জিনিসের কী নাম, কোন কাজে কোন method, ভুল হলে কোন status code, বড় তালিকা কীভাবে ভাগ হবে, আর বদলানোর সময় পুরোনো app যেন না ভাঙে। HTTP বার্তার খুঁটিনাটি আছে Computer Networks কোর্সের HTTP পর্বে, আর DNS, TCP ও TLS পর্ব ০৭-এ; এখানে শুধু নকশার সিদ্ধান্ত।

পুরো পর্বে একটি উদাহরণ সাথে থাকবে: একটি আন্তঃজেলা বাস-টিকিট বুকিং সেবা (কাল্পনিক)। যাত্রী app-এ ট্রিপ খোঁজেন, কয়েক মিনিটের জন্য সিট ধরে রাখেন, মোবাইল ওয়ালেটে টাকা দেন; কাউন্টারের কর্মীও একই বুকিং বদলাতে পারেন। ঈদের আগের ভিড়, দুর্বল নেটওয়ার্কে বারবার retry, হালনাগাদ না করা পুরোনো app — API নকশার প্রায় সব কঠিন প্রশ্ন এখানে। টাকার খাতা আর ভিড়ে সিট বিক্রির পুরো নকশা পর্ব ৬০-এর case study-তে; এখানে শুধু API-র স্তর।

পুরোনো দিনের ডাকঘরের কথা ভাবুন। প্রতিটি বাড়ির একটি স্থায়ী ঠিকানা, আর খামের ওপর অল্প কয়েকটি মানক নির্দেশ — সাধারণ ডাক, রেজিস্ট্রি, না পেলে ফেরত। নতুন বাড়ির জন্য ডাকপিয়নকে নতুন নিয়ম শিখতে হয় না। REST-এর মূল ভাবনাও তাই: প্রতিটি জিনিসের একটি ঠিকানা, আর সব জিনিসের ওপর একই অল্প কয়েকটি কাজ।

REST-এর তিনটি মূল শব্দ। Resource হলো যেকোনো জিনিস যার নাম দেওয়া যায় — একটি ট্রিপ, একটি বুকিং, বা একজন যাত্রীর সব বুকিংয়ের তালিকা। তার ঠিকানা URI, যেমন /v1/bookings/bk_1001. আর তারে যা যায় তা resource নিজে নয়, তার একটি representation — সাধারণত JSON-এ লেখা সেই মুহূর্তের একটি ছবি। একই বুকিংয়ের JSON রূপ app-এর জন্য, PDF রূপ টিকিট ছাপার জন্য — resource এক, রূপ দুটি।

Roy Fielding ছিলেন HTTP/1.1 মানদণ্ডের লেখকদের একজন। ২০০০ সালে University of California, Irvine-এ জমা দেওয়া তাঁর PhD গবেষণাপত্র Architectural Styles and the Design of Network-based Software Architectures-এর পঞ্চম অধ্যায়ে তিনি ওয়েবের সফল নকশাকে কয়েকটি শর্তে বেঁধে নাম দেন REST — নতুন protocol নয়, ওয়েব কেন টিকে আছে তার ব্যাখ্যা।

Fielding-এর শর্তঅর্থনকশায় যা পাই
Client-serverপর্দা আর ডেটা আলাদাদুই দিক আলাদাভাবে বদলায়
Statelessপ্রতিটি অনুরোধে দরকারি সব তথ্যযেকোনো সার্ভার যেকোনো অনুরোধ সামলায়; load balancer-এর পেছনে কপি বাড়ানো সহজ (পর্ব ১৩, ১৫)
Cacheউত্তর বলে cache করা যাবে কি নাCDN আর ব্রাউজার ভার কমায় (অংশ ৮)
Uniform interfaceসব resource-এ একই method, একই নিয়মমাঝের সব যন্ত্র অনুরোধ বোঝে
Layered systemমাঝে proxy, gateway বা CDNclient স্তর জানে না (পর্ব ১১)
Code on demand (ঐচ্ছিক)server কোডও পাঠাতে পারেব্রাউজারে JavaScript

Uniform interface-এর ভেতরে সবচেয়ে কড়া শর্তটির নাম HATEOAS: উত্তরের ভেতরেই থাকবে পরের কাজের link — যেমন সিট ধরে রাখার উত্তরে “পরিশোধ করুন” আর “বাতিল করুন”-এর ঠিকানা। ওয়েবসাইট ঠিক এভাবে চলে: আপনি কোনো URL মুখস্থ রাখেন না, link আর form ধরে এগোন। Fielding ২০০৮ সালের একটি ব্লগ লেখায় স্পষ্ট বলেন, hypertext ছাড়া কোনো API-কে REST বলা উচিত নয়।

বাস্তবে বেশিরভাগ “REST API” এই শর্ত মানে না: URL-এর ছক ডকুমেন্টেশনে থাকে, app সেটি কোডে বেঁধে রাখে। এগুলো আসলে REST-ধাঁচের HTTP-JSON API — তবু resource, method আর status code-এর সুবিধা পুরোপুরিই পাওয়া যায়। Leonard Richardson-এর একটি মাপকাঠি, যা Martin Fowler-এর ২০১০ সালের একটি লেখায় জনপ্রিয় হয় — Richardson maturity model — চার স্তরে ভাগ করে: ০, এক URL-এ সব কাজ POST দিয়ে; ১, আলাদা resource; ২, ঠিক HTTP method আর status code; ৩, hypermedia link. এই পর্বের নকশা স্তর ২-এ; link শুধু Location আর পরের পাতার cursor-এ।

মূল ধারণা

REST একটি স্থাপত্য-রীতি (architectural style), protocol বা মানদণ্ড নয়। “এটা কি সত্যিকারের REST?” তর্কের চেয়ে জরুরি প্রশ্ন: API-টি কি অনুমানযোগ্য? একটি endpoint শিখলে কি বাকিগুলো আন্দাজ করা যায়?

২ · Method-এর অর্থ — safe, idempotent আর নিরাপদ retry

HTTP method একটি প্রতিশ্রুতি: ব্রাউজার, proxy, CDN আর retry-র লাইব্রেরি method দেখেই ঠিক করে উত্তর জমা রাখা বা আবার পাঠানো যাবে কি না। দুটি গুণ মনে রাখুন। Safe মানে client শুধু পড়তে চায়, server-এর অবস্থা বদলাতে চায় না। Idempotent মানে একবার পাঠানো আর দশবার পাঠানোর চূড়ান্ত প্রভাব এক।

MethodকাজSafeIdempotentনকশায় এর মানে
GETresource পড়াহ্যাঁহ্যাঁcache করা যায়, retry নিরাপদ; কখনো অবস্থা বদলাবে না
HEADGET-এর মতোই, শুধু headerহ্যাঁহ্যাঁbody না এনে ETag বা আকার দেখা
PUTএই URI-তে পুরো রূপ বসানোনাহ্যাঁretry নিরাপদ; client আগেই ঠিকানা জানে
DELETEresource মোছানাহ্যাঁদ্বিতীয়বার 404 আসতে পারে, কিন্তু অবস্থা একই
POSTনতুন তৈরি বা কোনো প্রক্রিয়ানানাretry বিপজ্জনক — idempotency key লাগে (অংশ ৭)
PATCHঅংশবিশেষ বদলনাসাধারণভাবে না“সিট C2 করুন” idempotent, “একটি ব্যাগ যোগ করুন” নয়

OPTIONS আর method-এর protocol-স্তরের খুঁটিনাটি আছে HTTP পর্বের অংশ ৩-এ। নকশার চোখে তিনটি শিক্ষা:

  • Idempotent মানে একই উত্তর নয়, একই অবস্থা। একটি বুকিং DELETE করলে প্রথমবার 204, দ্বিতীয়বার হয়তো 404 — কিন্তু দুবারের পরেও server-এর অবস্থা এক: বুকিংটি নেই।
  • GET দিয়ে কখনো কিছু বদলাবেন না। GET /cancelBooking?id=1001 বিপজ্জনক: ব্রাউজারের prefetch বা ইমেইলের link-যাচাইকারী নিজে থেকেই GET পাঠাতে পারে, CDN উত্তর জমিয়ে রাখতে পারে।
  • তৈরি করতে POST, না PUT? Server id ঠিক করলে POST /v1/bookings, উত্তরে 201 Created আর Location. Client নিজে অনন্য id বানালে PUT /v1/drafts/{draft_id} — idempotent, তাই retry-তে দ্বিতীয় খসড়া হয় না।

PATCH-এর body-র রূপও চুক্তির অংশ। সবচেয়ে সরল প্রমিত রূপ JSON Merge Patch (RFC 7396): শুধু বদলানো ঘর পাঠান, যেমন {"boarding": "Kallyanpur"}; কোনো ঘরে null পাঠালে সেটি মুছে যায়। আর CRUD-এর সাথে method-এর মিল — POST তৈরি, GET পড়া, PUT বা PATCH বদল, DELETE মোছা — একটি শুরু মাত্র; সব কাজ CRUD-এ পড়ে না, পরের অংশেই তা দেখব।

৩ · Resource আর URI-র নাম — বিশেষ্য, বহুবচন আর nesting-এর সীমা

ভালো URI পড়লেই বোঝা যায় কী পাওয়া যাবে, আর একটি endpoint দেখে বাকিগুলো আন্দাজ করা যায়। নিচের নিয়মগুলো আইন নয়, প্রচলিত রীতি; আসল নিয়ম একটাই — পুরো API-তে একই রীতি মানা।

নিয়মভালোএড়িয়ে চলুন
বিশেষ্য, ক্রিয়া নয় — কাজ বলে methodPOST /v1/bookingsPOST /v1/createBooking
Collection বহুবচনে, একটি জিনিস তার id-তে/v1/trips, /v1/trips/{trip_id}/v1/trip/getAll
Nesting বড়জোর এক স্তর, যখন সন্তান মা-ছাড়া অর্থহীন/v1/trips/{trip_id}/seatsচার-পাঁচ স্তরের লম্বা path
Path চেনায় resource; filter, sort আর পাতা আসে query-তে/v1/bookings?status=held/v1/bookings/held/page/2
ছোট হাতের অক্ষর, শব্দের মাঝে hyphen, ফাইলের extension নয়/v1/seat-maps/v1/SeatMaps.json
id স্থায়ী আর অস্বচ্ছ, ধরন বোঝাতে উপসর্গbk_1001 (বাস্তবে এলোমেলো অংশসহ)মোট বুকিং-সংখ্যা ফাঁস করা ক্রমিক নম্বর

CRUD-এ না পড়া কাজ — ধরে রাখা, বাতিল আর ফেরত

বুকিং বাতিল কি DELETE? মুছে ফেললে ইতিহাস আর ফেরতের হিসাব যায়। দুটি প্রচলিত পথ: কাজটিকেই resource বানানো — POST /v1/bookings/{id}/cancellation একটি বাতিল-রেকর্ড তৈরি করে; অথবা “custom method” — POST /v1/bookings/{id}:cancel, যেমনটি Google-এর প্রকাশিত API নির্দেশিকায় (AIP) দেখা যায়। একটি বেছে পুরো API-তে মানুন। সিট ধরে রাখা মডেল হয় বুকিং নিজেই হিসেবে: "status": "held" আর মেয়াদ expires_at; মেয়াদ পেরোলে server সিট ছেড়ে দেয়।

এবার পুরো সেবার endpoint ধাপে ধাপে — প্রতি ধাপে একটি প্রশ্ন, একটি সিদ্ধান্ত:

tripট্রিপ — রুট, তারিখ, বাস
seatট্রিপের সন্তান
bookingheld → confirmed
paymentবুকিংয়ের সন্তান
cancellationবাতিল-রেকর্ড
refundদীর্ঘ কাজ
এই ধাপে
প্রশ্ন
সিদ্ধান্ত
endpoint
  1. ধাপ ১ — বিশেষ্য খোঁজা। Requirement-এর বাক্য থেকে বিশেষ্যগুলো তুলুন: যাত্রী ট্রিপ খোঁজেন, সিট বাছেন, বুকিং করেন, টাকা দেন, দরকারে বাতিল করে টাকা ফেরত নেন। পাঁচটি resource: trip, seat, booking, payment আর refund. যাত্রীর পরিচয় আসবে টোকেন থেকে।
  2. ধাপ ২ — খোঁজা মানে collection-এ শর্ত। “ঢাকা থেকে রাজশাহী, ২ নভেম্বর” নতুন resource নয়, ট্রিপের তালিকার ওপর শর্ত: GET /v1/trips আর query parameter. GET বলে CDN কয়েক সেকেন্ড উত্তর জমা রাখতে পারে।
  3. ধাপ ৩ — nesting কতটা? সিট ট্রিপ ছাড়া অর্থহীন, তাই এক স্তর nesting: /v1/trips/{trip_id}/seats. কিন্তু বুকিং নিজেই বড় জিনিস, id দিয়ে সরাসরি খোঁজা হয় — তাই উপরের স্তরে, ট্রিপ আর সিট থাকে body-তে।
  4. ধাপ ৪ — ধরে রাখা আর টাকা। POST /v1/bookings সিটটি ১০ মিনিটের জন্য ধরে রাখা একটি বুকিং বানায়, উত্তরে 201 আর Location; সিট আগেই বিক্রি হলে 409. টাকা দেওয়া বুকিংয়ের সন্তান, সাথে বাধ্যতামূলক Idempotency-Key (অংশ ৭)।
  5. ধাপ ৫ — বাতিল মানে মোছা নয়। ইতিহাস আর ফেরতের হিসাব থাকা চাই, তাই বাতিল একটি নতুন resource. টাকা ফেরতে সময় লাগে, তাই উত্তর 202 আর ফেরতের অবস্থা দেখার ঠিকানা (অংশ ৯)।
  6. ধাপ ৬ — নিজের তালিকা। যাত্রী শুধু নিজের বুকিং দেখবেন: GET /v1/me/bookings, cursor দিয়ে পাতা, filter আর sort (অংশ ৫)। কাউন্টারের কর্মীর জন্য GET /v1/bookings?trip_id=… — একই resource, ভিন্ন অনুমতি।

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

Method আর pathকাজসফল হলেপ্রধান ত্রুটি
GET /v1/trips?from=&to=&date=ট্রিপ খোঁজা200 + তালিকা400, 422 (অতীতের তারিখ)
GET /v1/trips/{trip_id}/seatsসিটের অবস্থা200 + ETag404
POST /v1/bookingsসিট ধরে রাখা201 + Location409 (সিট নেই), 422
GET /v1/bookings/{id}একটি বুকিং200 + ETag404
PATCH /v1/bookings/{id}ওঠার জায়গা বদল200412, 422
POST /v1/bookings/{id}/paymentsটাকা দেওয়া201400 (key নেই), 409, 422
POST /v1/bookings/{id}/cancellationবাতিল202 + ফেরতের ঠিকানা409 (যাত্রা শুরু হয়ে গেছে)
GET /v1/refunds/{refund_id}ফেরতের অবস্থা200404
GET /v1/me/bookingsনিজের বুকিং200 + next_cursor401

সব endpoint-এর জন্য সাধারণ: টোকেন নেই বা ভুল হলে 401, অনুমতি না থাকলে 403, সীমার বেশি অনুরোধে 429, আর server-এর দিকের ব্যর্থতায় ৫xx.

৪ · Status code বাছাই আর এক ছাঁচের ত্রুটি-বার্তা

Status code আসলে যন্ত্রের জন্য লেখা — app-এর retry-কোড, gateway, CDN আর monitoring সংখ্যাটি দেখেই সিদ্ধান্ত নেয়; মানুষের জন্য বিস্তারিত থাকে body-তে। তাই সব উত্তরে 200 দিয়ে body-তে “error” লেখা চলে না; এই খারাপ অভ্যাস আর সব কোডের পূর্ণ তালিকা আছে HTTP পর্বের অংশ ৪-এ। নকশাকারীর কাজ প্রতিটি পরিস্থিতির জন্য কোড বেছে চুক্তিতে লেখা। বাস-টিকিট সেবায়:

পরিস্থিতিকোডসাথে যা দেবেনClient কী করবে
পড়া সফল200 OKbody, দরকারে ETagদেখাবে
নতুন বুকিং তৈরি201 CreatedLocation: /v1/bookings/bk_1002ঠিকানা মনে রাখবে
ফেরতের কাজ নেওয়া হয়েছে, শেষ হয়নি202 Acceptedঅবস্থা দেখার ঠিকানাপরে দেখবে
সফল, ফেরত দেওয়ার কিছু নেই204 No Content—এগোবে
JSON ভাঙা, ঘরের ধরন ভুল400 Bad Requestকোন ঘরে সমস্যানা বদলে আবার নয়
টোকেন নেই বা মেয়াদোত্তীর্ণ401 UnauthorizedWWW-Authenticateআবার লগইন
পরিচয় জানা, অনুমতি নেই403 Forbidden—আর চেষ্টা নয়
নেই, বা দেখার অধিকারই নেই404 Not Found——
সিট আগেই বিক্রি, বা একই key-এর কাজ চলছে409 Conflictবর্তমান অবস্থাঅন্য সিট, বা একটু পরে
If-Match-এর ETag পুরোনো412 Precondition Failedনতুন ETagনতুন করে পড়ে আবার
গঠন ঠিক, নিয়ম ভাঙা (অতীতের তারিখ)422 Unprocessable Contentকোন নিয়মইনপুট বদলাবে
সীমার বেশি অনুরোধ429 Too Many RequestsRetry-Afterততক্ষণ অপেক্ষা
Server-এর বাগ500 Internal Server Errortrace id মাত্রসীমিত retry, শুধু নিরাপদ অনুরোধে
Gateway পেছন থেকে ভাঙা উত্তর পেল502 Bad Gateway—backoff-সহ retry
সাময়িক চাপ বা রক্ষণাবেক্ষণ503 Service UnavailableRetry-Afterততক্ষণ অপেক্ষা
Gateway সময়মতো উত্তর পায়নি504 Gateway Timeout—ফল অজানা — key থাকলে তবেই retry

তিনটি চিরকালের তর্ক। 401 না 403? 401 মানে “আপনি কে, জানি না”; নামটি “Unauthorized” হলেও অর্থ authentication. 403 মানে “জানি আপনি কে, কিন্তু অনুমতি নেই।” 404 দিয়ে লুকানো: অন্য যাত্রীর বুকিং id-তে 403 দিলে জানিয়ে দেওয়া হয় যে বুকিংটি আছে; তাই অনেক API তখন 404 দেয়। 400 না 422? গঠনই ভুল হলে 400; গঠন ঠিক কিন্তু অর্থে ভুল হলে 422 — RFC 9110 (২০২২) একে মূল HTTP-তে এনেছে “Unprocessable Content” নামে। অনেক দল সব validation-এ 400 দেয়; চলবে, যদি নিয়মটি লেখা থাকে আর সব জায়গায় একই হয়।

একটি অনুরোধে একাধিক সমস্যা থাকলে কোনটি আগে — টোকেন নেই, JSON-ও ভাঙা? যাচাইয়ের ক্রম ঠিক না থাকলে একই অনুরোধে একেক সার্ভার একেক কোড দেয়। নিচের simulator একটি নির্দিষ্ট ক্রমে যাচাই চালায়; চালানোর আগে প্রতিটি লাইনের কোড আন্দাজ করুন:

// Which status code? The checks run in a FIXED order: the order is part of the API design.
function choose(r) {
  if (r.overQuota)               return ["429", "Retry-After: 30"];
  if (!r.token)                  return ["401", "WWW-Authenticate: Bearer"];
  if (r.badJson)                 return ["400", "body is not valid JSON"];
  if (!r.found || !r.canSee)     return ["404", r.found ? "hide that it exists" : "no such booking"];
  if (!r.allowed)                return ["403", "can see it, may not change it"];
  if (r.invalid)                 return ["422", "well-formed but breaks a rule"];
  if (r.staleEtag)               return ["412", "If-Match failed: re-read first"];
  if (r.conflict)                return ["409", "clashes with current state"];
  if (r.upstream === "busy")     return ["503", "Retry-After: 60"];
  if (r.upstream === "slow")     return ["504", "gateway gave up waiting"];
  if (r.upstream === "garbage")  return ["502", "bad reply from upstream"];
  if (r.async)                   return ["202", "Location: /v1/refunds/rf_51"];
  if (r.created)                 return ["201", "Location: /v1/bookings/bk_1002"];
  if (r.noBody)                  return ["204", "done, nothing to return"];
  return ["200", "here is the body"];
}
const ok = { token: true, found: true, canSee: true, allowed: true };
const cases = [
  ["book seat B4 (new booking)",       { ...ok, created: true }],
  ["read my booking",                  { ...ok }],
  ["seat B4 already sold",             { ...ok, conflict: true }],
  ["travel date in the past",          { ...ok, invalid: true }],
  ["JSON missing a closing brace",     { ...ok, badJson: true }],
  ["no Authorization header",          { ...ok, token: false }],
  ["someone else's booking id",        { ...ok, canSee: false }],
  ["agent tries to edit fare table",   { ...ok, allowed: false }],
  ["edit with an old ETag",            { ...ok, staleEtag: true }],
  ["refund needs bank, ~2 days",       { ...ok, async: true }],
  ["payment gateway timed out",        { ...ok, upstream: "slow" }],
  ["booking DB overloaded",            { ...ok, upstream: "busy" }],
  ["350 requests in one minute",       { ...ok, overQuota: true }],
];
for (const [what, req] of cases) console.log(what.padEnd(32) + choose(req).join("  "));

আউটপুট:

book seat B4 (new booking)      201  Location: /v1/bookings/bk_1002
read my booking                 200  here is the body
seat B4 already sold            409  clashes with current state
travel date in the past         422  well-formed but breaks a rule
JSON missing a closing brace    400  body is not valid JSON
no Authorization header         401  WWW-Authenticate: Bearer
someone else's booking id       404  hide that it exists
agent tries to edit fare table  403  can see it, may not change it
edit with an old ETag           412  If-Match failed: re-read first
refund needs bank, ~2 days      202  Location: /v1/refunds/rf_51
payment gateway timed out       504  gateway gave up waiting
booking DB overloaded           503  Retry-After: 60
350 requests in one minute      429  Retry-After: 30

ক্রমটি এক নজরে: সীমা (429, প্রায়ই gateway-তেই) → পরিচয় (401) → গঠন (400) → অস্তিত্ব আর দেখার অধিকার (404) → বদলানোর অধিকার (403) → নিয়ম (422) → পূর্বশর্ত (412) → সংঘাত (409) → পেছনের সেবা (503, 504, 502) → সফলতার ধরন (202, 201, 204, 200). “someone else's booking id” পেল 404, 403 নয় — অস্তিত্ব ফাঁস না করতে। তালিকায় 500 নেই, কারণ 500 কোনো যাচাইয়ের সিদ্ধান্ত নয়, অপ্রত্যাশিত বাগের ফল। একটি case বদলে { ...ok, token: false, badJson: true } দিয়ে চালান: উত্তর 401, কারণ পরিচয়ের যাচাই আগে।

এক ছাঁচের ত্রুটি — RFC 9457 problem details

প্রতিটি endpoint নিজের মতো ত্রুটি লিখলে app-কে প্রতিটির জন্য আলাদা কোড লিখতে হয়। IETF-এর Problem Details মানদণ্ড একটি সাধারণ ছাঁচ দেয়, media type application/problem+json. লেখার সময়ে সর্বশেষ সংস্করণ RFC 9457 (২০২৩), যা আগের RFC 7807 (২০১৬)-কে প্রতিস্থাপন করেছে। পাঁচটি মানক ঘর — type (সমস্যার ধরনের একটি URI, যা ডকুমেন্টেশনে নিয়ে যায়), title, status, detail আর instance (এই নির্দিষ্ট ঘটনার পরিচয়) — আর দরকারে নিজের বাড়তি ঘর।

POST /v1/bookings HTTP/1.1
Content-Type: application/json

{ "trip_id": "DHK-RJH-1027", "seat": "B4" }

HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/seat-taken",
  "title": "Seat already taken",
  "status": 409,
  "detail": "Seat B4 on trip DHK-RJH-1027 was booked by another passenger.",
  "available_seats": ["B5", "C2"],
  "trace_id": "req_7f3a"
}

App পড়বে স্থির type, আর মানুষকে দেখাবে নিজের ভাষার বার্তা; trace_id দিয়ে support দল log-এ অনুরোধটি খুঁজে পায় (পর্ব ৫০); বাড়তি ঘর available_seats দেখে app সঙ্গে সঙ্গে বিকল্প দেখায়। আর 500-এ কখনো stack trace বা SQL নয় — সেটি আক্রমণকারীর জন্য মানচিত্র।

৫ · তালিকা ভাগে ভাগে — offset বনাম cursor, filter, sort আর sparse field

ঈদের আগের সপ্তাহে একটি জনপ্রিয় রুটে কয়েক লাখ বুকিং হতে পারে (ধরা যাক)। সব একবারে পাঠালে উত্তর বিশাল আর ধীর, server-এর মেমরিও ফুরায়। তাই pagination: প্রতি উত্তরে একটি ছোট পাতা, সাথে পরের পাতার চাবি। ডিফল্ট আর সর্বোচ্চ মাপ চুক্তিতে লিখুন — ধরা যাক ২০ আর ১০০; কেউ limit=100000 চাইলেও ১০০-তেই থামুন।

Offset — “প্রথম ৪০টি বাদ দিয়ে পরের ২০টি”

?limit=20&offset=40. সহজ, “পাতা ৭-এ যান” চলে। দুর্বলতা: গভীর পাতায় ধীর, আর পড়ার মাঝে যোগ-মোছায় পুনরাবৃত্তি বা বাদ।

ছোট, প্রায় স্থির তালিকা

Cursor — “যেখানে থেমেছিলাম, তার পর থেকে”

?limit=20&cursor=eyJpZCI6OTk5OTZ9. Index ধরে সরাসরি পৌঁছায়, মাঝের পরিবর্তনে স্থির। দুর্বলতা: সরাসরি পাতা ৭-এ লাফ নেই, আর sort key অনন্য ও indexed হতে হবে।

feed, বড় তালিকা, sync

Offset pagination-এ ডেটাবেস OFFSET 50000 পেতে আগের ৫০,০০০ সারি সত্যিই একটা একটা করে পেরিয়ে ফেলে দেয় — offset যত বড়, কাজ তত বেশি; শেষের দিকের পাতা সবচেয়ে ধীর। Keyset pagination আগের পাতার শেষ সারির key মনে রাখে — “id ৯৯৯৯৬-এর চেয়ে ছোট পরের ৫টি” — আর index-এর B-tree ধরে সোজা সেখানে নামে; প্রথম আর লাখতম পাতার খরচ প্রায় সমান। দ্বিতীয় সমস্যা আরও সূক্ষ্ম: offset গোনে অবস্থান, আর অবস্থান বদলায়। পাতা ১ পড়ার পর মাথায় নতুন বুকিং এলে সবাই এক ঘর নিচে নামে, পাতা ২-এর প্রথম সারিটি আগের পাতারই শেষ সারি। মাঝে একটি সারি মুছে গেলে উল্টো — একটি সারি ওপরে উঠে আগের পাতার এলাকায় চলে যায়, পাঠক সেটি কখনো দেখেন না।

নিচের লাইভ SQL এডিটরে ১ লাখ বুকিংয়ের একটি টেবিল (“নমুনা ডেটাবেস” খুলে দেখতে পারেন): প্রথমে দুই পদ্ধতির query plan, তারপর পাতা পড়ার মাঝে একটি নতুন বুকিং আর একটি বাতিল। এটি single-node SQLite ডেমো — query plan আর কোন সারি এলো তা দেখায়; সময় মাপে না, বহু যন্ত্রে ভাগ করা ডেটাবেসের আচরণও দেখায় না। EXPLAIN QUERY PLAN query না চালিয়েই পথটি বলে।

-- Newest first, 5 per page. Sample table: 100,000 bookings, ids 1..100000.
-- 1) How does each method reach a deep page?
EXPLAIN QUERY PLAN
SELECT id, seat FROM bookings ORDER BY id DESC LIMIT 5 OFFSET 50000;
EXPLAIN QUERY PLAN
SELECT id, seat FROM bookings WHERE id < 50001 ORDER BY id DESC LIMIT 5;

-- 2) A reader fetches page 1 ...
SELECT group_concat(id, ' ') AS page_1
FROM (SELECT id FROM bookings ORDER BY id DESC LIMIT 5);
-- ... then a NEW booking arrives at the top.
INSERT INTO bookings VALUES (100001, '2026-10-10 09:00:00', 'DHK-RJH', 'B4');
SELECT group_concat(id, ' ') AS offset_page_2
FROM (SELECT id FROM bookings ORDER BY id DESC LIMIT 5 OFFSET 5);
SELECT group_concat(id, ' ') AS keyset_page_2       -- cursor: last id seen = 99996
FROM (SELECT id FROM bookings WHERE id < 99996 ORDER BY id DESC LIMIT 5);

-- 3) ... then booking 99993 is cancelled and deleted.
DELETE FROM bookings WHERE id = 99993;
SELECT group_concat(id, ' ') AS offset_page_3
FROM (SELECT id FROM bookings ORDER BY id DESC LIMIT 5 OFFSET 10);
SELECT group_concat(id, ' ') AS keyset_page_3       -- cursor: last id seen = 99991
FROM (SELECT id FROM bookings WHERE id < 99991 ORDER BY id DESC LIMIT 5);

প্রথম plan-এর detail কলামে SCAN bookings: offset-এর query টেবিলটি উল্টো ক্রমে গোড়া থেকে হাঁটে, ৫০,০০০ সারি ফেলে তবে ৫টি দেয়। দ্বিতীয়টিতে SEARCH bookings USING INTEGER PRIMARY KEY (rowid<?): B-tree-তে সোজা ৫০,০০১-এর আগে নেমে ৫টি পড়ে — গভীরতা যা-ই হোক। তারপর পাতাগুলো:

page_1          100000 99999 99998 99997 99996
✓ INSERT — ১টি সারি বদলেছে
offset_page_2   99996 99995 99994 99993 99992
keyset_page_2   99995 99994 99993 99992 99991
✓ DELETE — ১টি সারি বদলেছে
offset_page_3   99990 99989 99988 99987 99986
keyset_page_3   99990 99989 99988 99987 99986

Offset পাঠকের পাতা ২ শুরু হলো ৯৯৯৯৬ দিয়ে — পাতা ১-এর শেষ সারিটি আবার। পাতা ৩ দুই পদ্ধতিতে হুবহু এক দেখালেও offset পাঠক ৯৯৯৯১ কখনো পাননি: ৯৯৯৯৩ মুছে যাওয়ায় সেটি পাতা ২-এর এলাকায় উঠে গেছে। Keyset পাঠক প্রতিটি সারি ঠিক একবার পেলেন।

একই ঘটনা ছোট সংখ্যায়, পাতা ধরে ধরে — দুজন পাঠক, একজন offset-এ, আরেকজন cursor-এ:

একটি ট্রিপের বুকিং, নতুন আগে · পাতার মাপ ৩
তালিকা এখন
Offset পাঠক পেলেন
Cursor পাঠক পেলেন
Cursor-এর ভেতরে
  1. শুরু। একটি ট্রিপের ১০টি বুকিং, id ১০০১ থেকে ১০১০, নতুনগুলো আগে; পাতার মাপ ৩. একজন পাঠকের app offset দিয়ে পাতা আনে, আরেকজনের cursor দিয়ে।
  2. পাতা ১। দুজনেই পেলেন ১০১০, ১০০৯ আর ১০০৮. Offset পাঠকের পরের অনুরোধ offset=3; cursor পাঠকের টোকেনে লেখা “১০০৮-এর পর থেকে”।
  3. নতুন বুকিং ১০১১। পাতা ২ চাওয়ার আগেই একজন যাত্রী বুকিং করলেন; তালিকার মাথায় ১০১১, বাকি সবাই এক ঘর নিচে নামল।
  4. পাতা ২। Offset পাঠক অবস্থান ৩ থেকে পেলেন ১০০৮, ১০০৭ আর ১০০৬ — ১০০৮ দ্বিতীয়বার! Cursor পাঠক “১০০৮-এর চেয়ে ছোট” চেয়ে পেলেন ১০০৭, ১০০৬ আর ১০০৫.
  5. বুকিং ১০০৬ বাতিল। সারিটি তালিকা থেকে সরে গেল; তার নিচের সবাই এক ঘর ওপরে উঠল।
  6. পাতা ৩। Offset পাঠক অবস্থান ৬ থেকে পেলেন ১০০৪, ১০০৩ আর ১০০২ — ১০০৫ ওপরে উঠে আগের পাতার এলাকায় চলে গেছে, তিনি এটি কখনো দেখবেন না। Cursor পাঠকও পেলেন একই তিনটি, কিন্তু তাঁর ১০০৫ আগের পাতাতেই দেখা।
  7. পাতা ৪ আর হিসাব। দুজনেই পেলেন ১০০১; cursor-এর উত্তরে next_cursor: null. Offset পাঠকের ১০টি সারিতে ১০০৮ দুবার, ১০০৫ নেই; cursor পাঠক প্রতিটি বুকিং ঠিক একবার দেখলেন। নতুন ১০১১ দেখা যাবে তালিকা মাথা থেকে আবার খুললে।

Cursor-এর নকশা — অস্বচ্ছ, অনন্য, indexed

  • অস্বচ্ছ টোকেন: cursor-এর ভেতরে শেষ key আর চলতি filter-sort লিখে base64 করুন, দরকারে signature দিন। Client শুধু ফেরত পাঠাবে — তাহলে ভেতরের ছক পরে বদলানো যায়।
  • Sort key অনন্য হতে হবে: শুধু departure_at ধরে sort করলে একই সময়ের দুটি ট্রিপের একটি পাতার সীমানায় হারাতে পারে। শেষে id জুড়ে দিন — ORDER BY departure_at, id, শর্ত (departure_at, id) > (:t, :id), আর সেই ক্রমের একটি composite index (পর্ব ২১)।
  • উত্তরের রূপ: {"items": […], "next_cursor": "…", "has_more": true}, অথবা Link header-এ rel="next" (RFC 8288).
  • মোট সংখ্যা দামি: প্রতি পাতায় COUNT(*) মানে প্রতিবার পুরো তালিকা গোনা; দরকারে আনুমানিক সংখ্যা দিন।
  • Offset কখন ঠিক: ছোট, প্রায় স্থির তালিকায়, গভীরতার একটি সীমা টেনে।

Filter, sort আর sparse field

তালিকার ওপর শর্ত দেয় query parameter:

GET /v1/bookings?trip_id=DHK-RJH-1027&status=confirmed
    &sort=-created_at&fields=id,seat,status&limit=20

sort=-created_at-এর বিয়োগ চিহ্ন মানে উল্টো ক্রম — একটি প্রচলিত রীতি। fields দিয়ে sparse fieldset: শুধু তিনটি ঘর — মোবাইল ডেটায় বড় সাশ্রয়। দুটি সাবধানতা। কোন ঘরে filter আর sort চলবে, তার নির্দিষ্ট তালিকা রাখুন, কারণ প্রতিটির পেছনে একটি index লাগে। আর অচেনা parameter এলে চুপচাপ উপেক্ষা না করে 400 দিন — নইলে একটি বানান ভুল (staus=) নিঃশব্দে পুরো তালিকা ফিরিয়ে দেবে। ঘর বাছাইয়ের ধারণা আরও এগিয়ে নিয়েছে GraphQL (পর্ব ০৯)।

৬ · Versioning আর সামঞ্জস্য — পুরোনো app না ভেঙে বদলানো

অনেকে মাসের পর মাস app হালনাগাদ করেন না। তাই API একটি চুক্তি: আজকের কোনো ঘর কাল হঠাৎ সরালে লাখো ফোনে app ভেঙে যাবে। Backward compatibility রাখার মূল নিয়ম: যোগ করুন, ভাঙবেন না।

পরিবর্তননিরাপদ?কেন
উত্তরে নতুন ঐচ্ছিক ঘর (যেমন bus_type)সাধারণত হ্যাঁপুরোনো client অচেনা ঘর উপেক্ষা করে — যদি সে tolerant reader হয়
নতুন endpointহ্যাঁপুরোনো কেউ সেটি ডাকেই না
Request-এ নতুন ঐচ্ছিক parameter, পুরোনো আচরণই ডিফল্টহ্যাঁনা পাঠালে আগের মতোই চলে
Enum-এ নতুন মান ("status": "waitlisted")সাবধানেকড়া client ভাঙতে পারে; চুক্তিতে আগেই লিখুন যে নতুন মান আসতে পারে
ঘর মোছা বা নাম বদলনাপুরোনো app ঘরটি খুঁজবে
ধরন বা একক বদল (টাকা থেকে পয়সা)নানিঃশব্দে ভুল হিসাব — সবচেয়ে বিপজ্জনক
ঐচ্ছিককে বাধ্যতামূলক করা, validation কড়া করানাআগে চলা অনুরোধ এখন 400 বা 422
ডিফল্ট sort বা pagination-এর অর্থ বদলনাঘর একই, আচরণ ভিন্ন

যে পরিবর্তন পুরোনো client ভাঙতে পারে, তা breaking change; তখন লাগে নতুন সংস্করণ — API versioning. সংস্করণ কোথায় লিখবেন, তার তিনটি প্রচলিত পথ:

URI-তে

/v1/bookings, /v2/bookings. সবচেয়ে প্রচলিত: চোখে পড়ে, পরীক্ষা সহজ, gateway-র route আর CDN-এর cache key সরল। দাম: সংস্করণ পুরো API-র; কড়া REST-পন্থীরা বলেন URI-র কাজ জিনিস চেনানো।

সরল, দৃশ্যমান

Header-এ

যেমন Api-Version: 2026-10-01 (নামটি API ভেদে আলাদা)। URI স্থির থাকে; কিছু প্রতিষ্ঠান তারিখভিত্তিক সংস্করণ দেয়, প্রতিটি ছোট breaking change একটি নতুন তারিখ। দাম: চোখে পড়ে না, আর cache-এ Vary লাগে।

স্থির URI

Media type-এ

Accept: application/vnd.example.v2+json. HTTP-র content negotiation-এর সাথে মেলে, resource ধরে আলাদা সংস্করণ রাখা যায়। দাম: লেখা, পরীক্ষা আর ডকুমেন্ট করা সবচেয়ে ঝামেলার।

শুদ্ধ, জটিল

বড় সংস্করণ বিরল রাখুন — প্রতিটি মানে দুটি কোড-পথ বছরের পর বছর চালানো। Schema evolution-এর নিয়ম আসবে পর্ব ১২-এ।

অবসরের পরিকল্পনা — Deprecation আর Sunset

পুরোনো সংস্করণ বন্ধের আগে যন্ত্রকেও জানাতে পুরোনো endpoint-এর উত্তরে দুটি header যোগ করুন — Deprecation আর Sunset:

HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: @1796083200
Sunset: Wed, 30 Jun 2027 23:59:59 GMT
Link: <https://developer.example.com/migrate-to-v2>; rel="deprecation"

Deprecation (RFC 9745, ২০২৫) জানায় endpoint-টি কবে থেকে পুরোনো — এখানে Unix সময় 1796083200, অর্থাৎ ১ ডিসেম্বর ২০২৬; Sunset (RFC 8594, ২০১৯) জানায় কোন মুহূর্তের পর এটি আর না-ও চলতে পারে। নম্বর আর খুঁটিনাটি লেখার সময়ের; ব্যবহারের আগে মূল RFC মিলিয়ে নিন। সাথে client id ধরে পুরোনো সংস্করণের ব্যবহার মাপুন, শেষের দিকের ব্যবহারকারীদের সরাসরি জানান, আর Sunset-এর পর উত্তর দিন 410 Gone. কিছু প্রতিষ্ঠান বন্ধের আগে ছোট পরিকল্পিত বিরতি (brownout) দেয়, যাতে ভুলে যাওয়া client-রা আগেভাগে টের পায়।

৭ · নিরাপদ retry — POST-এ idempotency key

App টাকা দেওয়ার অনুরোধ পাঠাল, ৫ সেকেন্ডে উত্তর এলো না — timeout. টাকা কি কেটেছে? অনুরোধ পথেই হারাতে পারে, কাজ শেষের পর উত্তরটি হারাতে পারে, বা server এখনো কাজ করছে — client-এর দিক থেকে তিনটিই এক রকম। আবার না পাঠালে হয়তো টিকিট হয়নি; পাঠালে হয়তো দুবার টাকা কাটবে। POST idempotent নয়, তাই লাগে একটি idempotency key.

ব্যাংকের কাউন্টারে আপনার হাতে একটি টোকেন। ক্যাশিয়ার টাকা নিলেন, কিন্তু রসিদ দেওয়ার আগেই বিদ্যুৎ চলে গেল। আলো ফিরলে একই টোকেন নিয়ে আবার দাঁড়ালেন। ভালো ক্যাশিয়ার খাতা দেখে বলবেন, “এই টোকেনের টাকা তো জমা হয়ে গেছে, এই নিন রসিদের কপি।” আর একই টোকেনে অন্য অঙ্কের স্লিপ দিলে বলবেন, “এই টোকেন অন্য লেনদেনের।” Idempotency key ঠিক এই টোকেন।

নিয়মটি কীভাবে চলে

  1. Client key বানায় — প্রতিটি অভিপ্রায়ে একবার। যাত্রী “পরিশোধ” চাপলেন — একটি অভিপ্রায়। App একটি UUID বানিয়ে Idempotency-Key header-এ পাঠায়, সব retry-তে একই key, আর চূড়ান্ত উত্তর না আসা পর্যন্ত key-টি ফোনে রাখে। প্রতি retry-তে নতুন key মানে পুরো ব্যবস্থাটাই অর্থহীন।
  2. Server key খোঁজে, না পেলে দাবি করে। (client, key) জোড়া UNIQUE constraint-সহ একটি টেবিলে, সাথে body-র hash — request fingerprint — আর অবস্থা “চলছে”। দুটি retry একসাথে এলেও ডেটাবেস একটিকেই ঢুকতে দেয়।
  3. কাজ, তারপর উত্তর জমা। টাকা কাটার পর status আর body key-এর পাশে জমা, অবস্থা “শেষ”।
  4. Retry এলে: কাজ শেষ আর hash মেলে — জমা উত্তর হুবহু; কাজ চলছে — 409; hash মেলে না — 422; বাধ্যতামূলক key-ই নেই — 400.
  5. মেয়াদ: সাধারণত কয়েক ঘণ্টা থেকে কয়েক দিন, API ভেদে আলাদা; তার পরে একই key নতুন অনুরোধ ধরা হতে পারে।

Header-টির নাম আর এই কোডগুলো এসেছে IETF-এর একটি খসড়া থেকে (The Idempotency-Key HTTP Header Field); লেখার সময়ে এটি চূড়ান্ত RFC হয়নি, যদিও বহু পেমেন্ট API-র প্রকাশিত ডকুমেন্টেশনে একই ধাঁচ দেখা যায়। পুরো পথটি ছবিতে:

Idempotency key-এর পথ — client থেকে payment gateway পর্যন্ত

Client → stateless API → booking DB (with an idempotency table) and payment service → external gateway

Idempotency key-এর পথ যাত্রীর app — client Booking API ×N Booking DB Payment service Payment gateway bookings idempotency_keys key একবার বানায় · retry-তে একই key stateless · আগে key দেখে, তারপর কাজ সিট · যাত্রী · অবস্থা key · body hash অবস্থা · জমা উত্তর একই key সামনে পাঠায় বাইরের সেবা · ধীর হতে পারে • একই body, কাজ শেষ → আগের উত্তর হুবহু • কাজ চলছে → 409 · body ভিন্ন → 422 retry এলে API key খোঁজে: ১ POST + key ৫ উত্তর · হারাতে পারে ২ key দাবি ৪ উত্তর জমা ৩ charge একই key
ছবি ৮.১ — কঠিন রেখা অনুরোধের পথ, ভাঙা রেখা উত্তর বা বাইরের কল। দুটি টেবিল একই ডেটাবেসে, একই transaction-এ লেখা হয়।

দুটি জিনিস লক্ষ করুন। এক, key আর বুকিং একই transaction-এ লেখা, তাই “বুকিং হলো কিন্তু উত্তর জমা হলো না” এমন আধা অবস্থা হয় না (পর্ব ২৪)। দুই, বাইরের payment gateway আমাদের transaction-এর বাইরে; তাই তাকেও একই key, বা তা থেকে বানানো key পাঠাতে হয়, যাতে আমাদের retry-তেও gateway দ্বিতীয়বার টাকা না কাটে। শিকলের প্রতিটি কড়া idempotent না হলে শিকল দুর্বল — গভীর আলোচনা পর্ব ৩৪-এ (delivery semantics), টিকিট-পেমেন্টের পূর্ণ নকশা পর্ব ৬০-এ। ঘটনাক্রম ধাপে ধাপে:

  1. Appclient
  2. APIstateless
  3. Key storeUNIQUE (client, key)
  4. Paymentservice
যাত্রীর দিক থেকে
ওয়ালেট
পর্দায়
  1. অভিপ্রায় একটি, key একটি। যাত্রী ৮৫০ টাকার টিকিটে “পরিশোধ” চাপলেন। App একটি নতুন key K1 বানিয়ে ফোনে রাখল।
  2. Key দাবি। App POST /v1/bookings/bk_1001/payments পাঠাল, header-এ K1. API store-এ K1 পেল না, তাই body-র hash আর অবস্থা “চলছে” লিখে key দাবি করল; UNIQUE constraint-এর কারণে একসাথে দুটি এলেও একটিই জিতবে।
  3. একবার টাকা কাটা। Payment service ৮৫০ টাকা কাটল, ওয়ালেটে এখন ৪,১৫০. উত্তর 201 pay_77 K1-এর পাশে জমা হলো।
  4. উত্তর হারাল। দুর্বল নেটওয়ার্কে উত্তর পথেই হারাল। ৫ সেকেন্ডে app পেল timeout — জানে না টাকা কেটেছে কি না।
  5. Backoff. App সঙ্গে সঙ্গে আবার পাঠায় না; প্রায় ১ সেকেন্ড আর একটু এলোমেলো সময় অপেক্ষা করে (exponential backoff with jitter), যাতে হাজারো app একসাথে retry storm না বাধায় (পর্ব ৪৮)।
  6. একই key, একই ফল। একই K1, একই body. API K1 পেল, hash মিলল, কাজ শেষ — জমা 201 pay_77 হুবহু ফেরত। Payment service-এ নতুন কিছু গেল না, ওয়ালেট ৪,১৫০-এই।
  7. অপব্যবহার ধরা পড়ল। ধরা যাক app-এর একটি বাগ একই K1 দিয়ে ৯০০ টাকার অনুরোধ পাঠাল। Hash মেলেনি, তাই 422. চুপচাপ পুরোনো ফল দিলে বাগটি লুকিয়ে থাকত।

এবার simulator-এ একই ঘটনা, সাথে কয়েকটি ভুল ব্যবহার। Key-এর store একটি Map, body-র fingerprint ৩২-বিটের FNV-1a hash, আর যাত্রীর ওয়ালেটে ৫,০০০ টাকা। ধাপ ১-এ প্রথম অনুরোধটি ধীর — কাজ চলতে চলতেই retry আসে:

// Idempotency-Key server simulator: one process, in-memory store, no randomness.
function fnv1a(s) {                                   // 32-bit FNV-1a hash of the request body
  let h = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
  return h.toString(16).padStart(8, "0");
}
const store = new Map();             // key -> { hash, reply }   (reply missing = still running)
let wallet = 5000, nextId = 77;      // passenger's wallet in Tk; next payment id

function handle(key, body, slow = false) {
  const hash = fnv1a(JSON.stringify(body)), rec = store.get(key);
  if (rec && rec.hash !== hash) return "422 key reused with a different body";
  if (rec && !rec.reply)        return "409 first attempt still running";
  if (rec)                      return rec.reply + "  (replayed)";
  store.set(key, { hash });                         // claim the key BEFORE the side effect
  return slow ? null : finish(key, body);
}
function finish(key, body) {                        // the side effect: charge exactly once
  wallet -= body.amount;
  const reply = "201 pay_" + nextId++ + " charged " + body.amount;
  store.get(key).reply = reply;
  return reply;
}
const pay = { booking: "bk_1001", amount: 850 };
const show = (step, r) => console.log(step.padEnd(30) + String(r ?? "(still working)").padEnd(38) + "wallet " + wallet);
console.log("body hash = " + fnv1a(JSON.stringify(pay)));
show("1 POST, key K1 (slow)",       handle("K1", pay, true));
show("2 retry K1 after timeout",    handle("K1", pay));
show("  server finishes K1",        finish("K1", pay) + "  (reply lost)");
show("3 retry K1 after backoff",    handle("K1", pay));
show("4 retry K1 again",            handle("K1", pay));
show("5 K1 but amount 900",         handle("K1", { ...pay, amount: 900 }));
show("6 new key K2, same body",     handle("K2", pay));
console.log("charges made: " + (nextId - 77) + "; without keys, steps 1-4 alone = 4 charges");

আউটপুট:

body hash = 459ba005
1 POST, key K1 (slow)         (still working)                       wallet 5000
2 retry K1 after timeout      409 first attempt still running       wallet 5000
  server finishes K1          201 pay_77 charged 850  (reply lost)  wallet 4150
3 retry K1 after backoff      201 pay_77 charged 850  (replayed)    wallet 4150
4 retry K1 again              201 pay_77 charged 850  (replayed)    wallet 4150
5 K1 but amount 900           422 key reused with a different body  wallet 4150
6 new key K2, same body       201 pay_78 charged 850                wallet 3300
charges made: 2; without keys, steps 1-4 alone = 4 charges

লাইন ২-এ প্রথম অনুরোধ তখনো চলছে, তাই 409 — দ্বিতীয় charge শুরুই হয়নি। তারপর server কাজ শেষ করল, ওয়ালেট ৪,১৫০, কিন্তু উত্তর হারাল। লাইন ৩ আর ৪-এ একই 201 pay_77 হুবহু ফেরত, টাকা আর কাটেনি; লাইন ৫-এ একই key, ভিন্ন অঙ্ক — 422. লাইন ৬ সবচেয়ে শিক্ষণীয়: নতুন key মানে server-এর চোখে নতুন অভিপ্রায় — দ্বিতীয় payment pay_78, ওয়ালেট ৩,৩০০. প্রতি retry-তে নতুন key বানানো app এভাবেই দুবার টাকা কাটে। এখানে store মেমরিতে; বাস্তবে ডেটাবেসে, যাতে সব API সার্ভার একই store দেখে।

৮ · একসাথে সম্পাদনা আর caching — ETag, If-Match আর 304

এবার অন্য সমস্যা: একই বুকিং দুজন একসাথে বদলাচ্ছেন। কাউন্টারের কর্মী সিট B4 থেকে C2 করলেন; প্রায় একই সময়ে যাত্রী app-এ ওঠার জায়গা গাবতলী থেকে কল্যাণপুর করলেন। দুজনই পুরোনো কপি পড়ে পুরো বুকিং PUT করলে পরের জন আগের জনের বদল মুছে দেন — lost update. কেউ ত্রুটি দেখেন না; যাত্রী বাসে উঠে দেখেন সিট আবার B4.

সমাধান optimistic concurrency: লক না ধরে সবাইকে পড়তে দিন, লেখার মুহূর্তে যাচাই করুন। HTTP-তে এর হাতিয়ার ETag — resource-এর সংস্করণের ছাপ। GET-এর উত্তরে server ETag দেয়; লেখার সময় client সেটি If-Match-এ ফেরত পাঠায় — একটি conditional request. না মিললে, অর্থাৎ মাঝে কেউ বদলেছে, উত্তর 412 Precondition Failed; client নতুন করে পড়ে, মিলিয়ে, আবার চেষ্টা করে। If-Match ছাড়া লেখা বন্ধ রাখতে চাইলে 428 Precondition Required (RFC 6585). W/ দিয়ে শুরু “weak” ETag If-Match-এ চলে না, কারণ লেখার শর্তে কড়া তুলনা লাগে।

// Optimistic concurrency with ETag + If-Match: two clients edit the same booking.
function fnv1a(s) {
  let h = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
  return h.toString(16).padStart(8, "0");
}
const db = { bk_1001: { seat: "B4", boarding: "Gabtoli" } };
const etag = (id) => '"' + fnv1a(JSON.stringify(db[id])) + '"';

function GET(id, ifNoneMatch) {
  if (ifNoneMatch === etag(id)) return { status: 304, etag: etag(id) };   // your copy is current
  return { status: 200, etag: etag(id), body: { ...db[id] } };
}
function PUT(id, body, ifMatch, checkPrecondition = true) {
  if (checkPrecondition && !ifMatch) return { status: 428 };               // If-Match required
  if (checkPrecondition && ifMatch !== etag(id)) return { status: 412, etag: etag(id) };
  db[id] = body;                                                           // full replacement
  return { status: 200, etag: etag(id) };
}
const log = (who, r) => console.log(who.padEnd(34) + r.status + "  " + (r.etag || "") +
  "  db=" + JSON.stringify(db.bk_1001));
const a = GET("bk_1001"); log("A (counter) GET", a);
const b = GET("bk_1001"); log("B (app) GET", b);
log("A PUT seat C2, If-Match old", PUT("bk_1001", { ...a.body, seat: "C2" }, a.etag));
log("B PUT boarding, If-Match old", PUT("bk_1001", { ...b.body, boarding: "Kallyanpur" }, b.etag));
log("B PUT with no If-Match", PUT("bk_1001", { ...b.body, boarding: "Kallyanpur" }));
const b2 = GET("bk_1001"); log("B re-reads (GET)", b2);
log("B PUT boarding, If-Match new", PUT("bk_1001", { ...b2.body, boarding: "Kallyanpur" }, b2.etag));
log("A GET If-None-Match old", GET("bk_1001", a.etag));
const c = GET("bk_1001"); log("A GET If-None-Match current", GET("bk_1001", c.etag));
db.bk_1001 = { seat: "B4", boarding: "Gabtoli" };                          // replay without checks
PUT("bk_1001", { ...a.body, seat: "C2" }, null, false);
log("no-check server, B then writes", PUT("bk_1001", { ...b.body, boarding: "Kallyanpur" }, null, false));

আউটপুট:

A (counter) GET                   200  "f52f8296"  db={"seat":"B4","boarding":"Gabtoli"}
B (app) GET                       200  "f52f8296"  db={"seat":"B4","boarding":"Gabtoli"}
A PUT seat C2, If-Match old       200  "4037343f"  db={"seat":"C2","boarding":"Gabtoli"}
B PUT boarding, If-Match old      412  "4037343f"  db={"seat":"C2","boarding":"Gabtoli"}
B PUT with no If-Match            428    db={"seat":"C2","boarding":"Gabtoli"}
B re-reads (GET)                  200  "4037343f"  db={"seat":"C2","boarding":"Gabtoli"}
B PUT boarding, If-Match new      200  "436016c8"  db={"seat":"C2","boarding":"Kallyanpur"}
A GET If-None-Match old           200  "436016c8"  db={"seat":"C2","boarding":"Kallyanpur"}
A GET If-None-Match current       304  "436016c8"  db={"seat":"C2","boarding":"Kallyanpur"}
no-check server, B then writes    200  "333cb88f"  db={"seat":"B4","boarding":"Kallyanpur"}

A আর B একই ETag "f52f8296" পড়লেন। A আগে লিখলেন — 200, নতুন ETag. B পুরোনো ETag নিয়ে লিখতে গিয়ে পেলেন 412, আর ডেটাবেসে A-র C2 অক্ষত; If-Match ছাড়া চেষ্টায় 428. B নতুন করে পড়ে লিখলেন — সিট C2 আর ওঠার জায়গা কল্যাণপুর, দুটি বদলই টিকল। চলতি ETag-এ If-None-Match পেল 304, body ছাড়া। শেষ লাইনই সতর্কবার্তা: শর্ত না দেখা server-এ B-র লেখা A-র সিট বদল মুছে আবার B4 করে দিল — lost update. এখানে ETag body-র hash; অনেক server version নম্বর থেকেও বানায়।

Server-এর ভেতরে If-Match সাধারণত চলে একটি version কলাম দিয়ে, আর যাচাই ও লেখা হয় একটিই UPDATE-এ — আলাদা SELECT আর UPDATE করলে দুইয়ের মাঝখানে আবার কেউ ঢুকে পড়তে পারে। এই compare-and-set নিচের লাইভ SQL-এ দেখুন:

-- Optimistic concurrency inside the server: the ETag maps to a version column.
CREATE TABLE bookings (id TEXT PRIMARY KEY, seat TEXT NOT NULL,
                       boarding TEXT NOT NULL, version INTEGER NOT NULL);
INSERT INTO bookings VALUES ('bk_1001', 'B4', 'Gabtoli', 1);

-- A (counter) and B (app) both read version 1. A writes first:
UPDATE bookings SET seat = 'C2', version = version + 1
WHERE id = 'bk_1001' AND version = 1;          -- 1 row changed: 200, new ETag "v2"
-- B still sends If-Match "v1":
UPDATE bookings SET boarding = 'Kallyanpur', version = version + 1
WHERE id = 'bk_1001' AND version = 1;          -- 0 rows changed: 412
SELECT id, seat, boarding, version FROM bookings;
-- B re-reads (version 2), merges its change, retries:
UPDATE bookings SET boarding = 'Kallyanpur', version = version + 1
WHERE id = 'bk_1001' AND version = 2;
SELECT id, seat, boarding, version FROM bookings;

একই শর্তের (version = 1) দুটি UPDATE: প্রথমটির ফল ১টি সারি বদলেছে, দ্বিতীয়টির ০টি সারি বদলেছে — server এই শূন্য দেখেই 412 দেয়। মাঝের SELECT দেখায় bk_1001 C2 Gabtoli 2, অর্থাৎ A-র বদল অক্ষত; B version 2 ধরে আবার লিখলে শেষ অবস্থা bk_1001 C2 Kallyanpur 3. যাচাই আর লেখা একটিই বাক্যে, তাই মাঝে কেউ ঢুকতে পারে না। তত্ত্ব আছে DBMS কোর্সের concurrency control পর্বে।

API-র cache header — কোন উত্তর কতক্ষণ

পড়ার দিকে ETag দেয় If-None-Match আর body-ছাড়া 304 (simulator-এর “If-None-Match current” লাইন)। যাচাই ছাড়া কপি কতক্ষণ চলবে, তা বলে Cache-Control-এর max-age — একটি TTL. API-তে তিন ধরনের উত্তর:

উত্তরCache-Control (উদাহরণ)কেন
ট্রিপের তালিকা আর সময়সূচি — সবার জন্য একpublic, max-age=60এক মিনিট পুরোনো হলেও ক্ষতি নেই; CDN ভিড় শুষে নেয়
সিটের অবস্থাno-cache আর ETagপ্রতিবার যাচাই, না বদলালে ছোট 304; শেষ কথা তো বুকিংয়ের মুহূর্তের 409
নিজের বুকিং, টিকিট, পেমেন্টprivate, no-storeব্যক্তিগত তথ্য — CDN বা শেয়ার করা cache-এ কখনো নয়

নামের ফাঁদ: no-cache মানে “ব্যবহারের আগে যাচাই”, জমা না রাখার নির্দেশ no-store. পুরো নিয়ম Computer Networks-এর ওয়েব caching পর্বে; CDN পর্ব ১৭-এ আর caching কৌশল পর্ব ২৮-এ।

৯ · দীর্ঘ কাজ, batch, ডকুমেন্টেশন আর নিরাপত্তা

দীর্ঘ কাজ — 202, অবস্থা-resource আর webhook

বাতিলের পর টাকা ফেরতে কয়েক মিনিট থেকে কয়েক দিন লাগে; এতক্ষণ সংযোগ খোলা রাখা যায় না, gateway অনেক আগেই 504 দেবে। তাই long-running operation-এর ছক: সঙ্গে সঙ্গে 202 Accepted আর একটি অবস্থা-resource-এর ঠিকানা; ভারী কাজ চলে পেছনের worker-এ, প্রায়ই queue থেকে — asynchronous processing (পর্ব ৩১, ৩৭)।

POST /v1/bookings/bk_1001/cancellation HTTP/1.1
Authorization: Bearer <token>
Idempotency-Key: 3f9a2c1e-7b4d-4e8a-9c2f-5d6e7f8a9b0c

HTTP/1.1 202 Accepted
Location: /v1/refunds/rf_51
Retry-After: 30
Content-Type: application/json

{ "refund_id": "rf_51", "status": "pending", "amount": 850 }

GET /v1/refunds/rf_51 HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json

{ "refund_id": "rf_51", "status": "succeeded", "completed_at": "2026-11-03T10:15:00Z" }

Client দুইভাবে ফল জানে। Polling: অবস্থা-resource-এ কিছুক্ষণ পরপর GET — সরল, কিন্তু অনেক বৃথা অনুরোধ; Retry-After বলে দেয় কতক্ষণ পরপর। Webhook: কাজ শেষ হলে server নিজেই client-এর নিবন্ধিত URL-এ POST পাঠায়। তার তিন নিয়ম: গোপন চাবিতে বানানো signature (যেমন HMAC), যাতে প্রাপক উৎস যাচাই করতে পারেন; না পৌঁছালে backoff-সহ আবার পাঠানো; আর প্রতিটি event-এর অনন্য id, কারণ একই event একাধিকবার পৌঁছাতে পারে — প্রাপককেও idempotent হতে হয়। সঙ্গে সঙ্গে খবর পৌঁছানোর অন্য পথ — long polling, SSE, WebSocket — পর্ব ১০-এ।

Batch endpoint — অনেক কাজ এক অনুরোধে

কাউন্টারের কর্মী একটি পরিবারের জন্য একসাথে ছয়টি সিট ধরতে চান। ছয়বার আসা-যাওয়ার বদলে একটি batch endpoint: POST /v1/bookings:batch. তিনটি সিদ্ধান্ত চুক্তিতে লিখুন। এক, সব-বা-কিছুই-না (atomic), না আংশিক সফলতা? পরিবারের সিটে সাধারণত সব-বা-কিছুই-না। দুই, আংশিক হলে প্রতিটি অংশের আলাদা status — অনেক API তখন 200 দিয়ে body-তে প্রতিটির ফল দেয়, কেউ কেউ WebDAV থেকে আসা 207 Multi-Status. তিন, সর্বোচ্চ কয়টি অংশ (ধরা যাক ৫০), আর পুরো batch-এর জন্য একটি idempotency key.

ডকুমেন্টেশন — OpenAPI দিয়ে চুক্তি লেখা

চুক্তি শুধু wiki-তে থাকলে কোড আর লেখা দ্রুত আলাদা হয়ে যায়। OpenAPI Specification চুক্তিটিকে যন্ত্র-পাঠযোগ্য YAML বা JSON-এ লেখে; একই ফাইল থেকে ডকুমেন্টেশনের পাতা, client-এর কোড (SDK), নকল server আর স্বয়ংক্রিয় contract test বানানো যায়। অনেক দল contract-first চলে: কোডের আগে OpenAPI ফাইল লিখে app আর backend দল মিলিয়ে নেয়। এর জন্ম Swagger নামে, ২০১০-এর দশকের শুরুতে; ২০১৫–১৬ সালে Linux Foundation-এর অধীন OpenAPI Initiative-এর হাতে গিয়ে নতুন নাম পায়। নিচে টাকা দেওয়ার endpoint-এর একটি অংশ, সংস্করণ 3.1-এর রূপে (খুঁটিনাটি সংস্করণভেদে আলাদা):

openapi: 3.1.0
info:
  title: Bus Booking API (fictional)
  version: 1.4.0
paths:
  /v1/bookings/{booking_id}/payments:
    post:
      summary: Pay for a held booking
      parameters:
        - name: booking_id
          in: path
          required: true
          schema: { type: string }
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, maxLength: 64 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, method]
              properties:
                amount: { type: integer, minimum: 1, description: Amount in Taka }
                method: { type: string, enum: [wallet, card] }
      responses:
        "201":
          description: Payment created; the same reply is replayed for a repeated key
        "400":
          description: Idempotency-Key header missing
        "409":
          description: Same key still in progress, or the hold has expired
        "422":
          description: Key reused with a different body, or a business rule is broken
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }

Idempotency-Key চুক্তিতেই বাধ্যতামূলক, আর 201-এর বর্ণনায় retry-র আচরণও লেখা। প্রতিটি পরিবর্তনে ফাইলটি আগের সংস্করণের সাথে স্বয়ংক্রিয়ভাবে তুলনা করলে breaking change merge-এর আগেই ধরা পড়ে।

নিরাপত্তার মূল নিয়ম — সংক্ষেপে

  • শুধু HTTPS: সাধারণ HTTP-তে টোকেন আর ব্যক্তিগত তথ্য তারে খোলা; এমন অনুরোধ প্রত্যাখ্যান করুন (TLS পর্ব)।
  • পরিচয় header-এ: Authorization: Bearer <token>; টোকেন কীভাবে আসে — OAuth 2.0 আর OpenID Connect — পর্ব ৫২-এ।
  • গোপন কিছু URL-এ নয়: টোকেন, OTP বা পাসওয়ার্ড query string-এ গেলে server log, ব্রাউজারের history আর Referer header-এ থেকে যায়।
  • প্রতিটি অনুরোধে বস্তুভিত্তিক অনুমতি: লগইন যাচাই যথেষ্ট নয়; GET /v1/bookings/bk_1001-এ দেখতে হবে বুকিংটি এই যাত্রীরই কি না — object-level authorization. OWASP-এর API Security Top 10-এ এর অভাব (Broken Object Level Authorization) সবার ওপরে, ২০১৯ আর ২০২৩ দুই সংস্করণেই।
  • সীমা আর আকার: client-প্রতি rate limit, পেরোলে 429 আর Retry-After (পর্ব ৪৯); body-র সর্বোচ্চ আকার (বেশি হলে 413); pagination-এর সর্বোচ্চ limit.
  • ত্রুটিতে কম কথা: 500-এ stack trace নয়; লগইনে “ভুল পাসওয়ার্ড” আর “এমন ব্যবহারকারী নেই” আলাদা করে বলবেন না।

এর অনেকটা এক জায়গায় প্রয়োগ করা যায় API gateway-তে (পর্ব ১১)।

আরও গভীরে: কয়েকটি বড় প্রতিষ্ঠান নিজেদের API নকশা-নির্দেশিকা প্রকাশ করেছে — Google-এর API Improvement Proposals (AIP), Microsoft-এর REST API Guidelines, Zalando-র RESTful API Guidelines. Resource-এর নাম, custom method, pagination-টোকেন আর long-running operation নিয়ে এদের সিদ্ধান্ত পাশাপাশি পড়লে দেখবেন, ভালো নকশাতেও মতভেদ আছে। Hypermedia-র জন্য HAL আর JSON:API-এর মতো ছাঁচও আছে।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • PUT, PATCH আর POST-এর পার্থক্য? — PUT পুরো রূপ বসায়, idempotent; PATCH অংশ বদলায়, সাধারণভাবে idempotent নয়; POST তৈরি বা প্রক্রিয়া করে, idempotent নয়।
  • Offset-এর বদলে cursor কেন? — গভীর পাতায় offset আগের সব সারি পেরোয়, আর পড়ার মাঝে সারি যোগ-মোছায় পুনরাবৃত্তি বা বাদ পড়ে; cursor index ধরে শেষ key-এর পর থেকে পড়ে।
  • টাকা পাঠানোর API-তে retry নিরাপদ করবেন কীভাবে? — Client-এর বানানো idempotency key; server-এ UNIQUE (client, key), body hash আর জমা উত্তর; ভিন্ন body-তে 422, কাজ চললে 409.
  • দুজন একসাথে একই রেকর্ড বদলালে? — ETag আর If-Match, না মিললে 412; ভেতরে version কলামে একটিই compare-and-set UPDATE.

১০ · AI-এর যুগে API নকশা — খসড়া AI-র, চুক্তি আপনার

AI-এর কাছে বাস-টিকিট বুকিংয়ের REST API চাইলে কয়েক সেকেন্ডে endpoint-এর তালিকা, এমনকি পুরো OpenAPI ফাইলও আসে। দেখতে গোছানো, কিন্তু কিছু চেনা ভুল বারবার দেখা যায়:

AI-এর ভুলএই পর্বের উদাহরণে কেমন দেখায়কীভাবে যাচাই করবেন
URL-এ ক্রিয়া, সব কাজ POSTPOST /bookTicketপ্রতিটি endpoint বিশেষ্য আর method-এ লিখুন
ত্রুটিতেও 200, বা 401 আর 403 গোলানো{"success": false}-সহ 200অংশ ৪-এর ছকে মেলান
অসীম তালিকায় offset?page= দিয়ে feedঅংশ ৫-এর SQL-এর মতো পরীক্ষা
“Idempotency যোগ করুন” — store, body hash, চলমান অবস্থা বা মেয়াদ ছাড়াদুটি retry একসাথে এলে দুবার টাকাএকই অনুরোধ একসাথে দুবার পাঠান
Failure আর concurrency উপেক্ষাETag নেই, lost updateদুই client আর একটি timeout হাতে চালান
RFC নম্বর, header বা পণ্যের সীমা ও দাম বানানোউৎসহীন “gateway সেকেন্ডে এত নেয়”মূল RFC আর অফিশিয়াল ডকুমেন্টেশন
হিসাবে গাণিতিক ভুলbandwidth-এ KB আর Kbit গোলানোনিজে আবার হিসাব (পর্ব ০৩)
Over-engineering — সবখানে একই ভারী উত্তরসাধারণ CRUD-এ GraphQL, Kafka, Redis আর microservicesপ্রতিটি টুলের পেছনের requirement জিজ্ঞেস করুন
ACID আর CAP-এর consistency গোলানো“cursor pagination তাই ACID”ACID-এর C: constraint রক্ষা; CAP-এর C: সব কপিতে সর্বশেষ মান (পর্ব ২৪, ৪০)

AI-কে নকশাকার নয়, কড়া reviewer বানান। আগে নিজে endpoint-এর খসড়া করুন, তারপর এভাবে যাচাই করান:

তুমি একজন অভিজ্ঞ API reviewer. নিচে একটি বাস-টিকিট বুকিং সেবার REST API-র খসড়া দিলাম (endpoint, status code, pagination, versioning): [এখানে আপনার খসড়া বা OpenAPI ফাইল পেস্ট করুন] ১. কোন endpoint-এর URL-এ ক্রিয়া আছে, আর কোথায় method-এর অর্থ (safe, idempotent) ভাঙা হয়েছে, দেখাও। ২. প্রতিটি ত্রুটির পরিস্থিতির status code যাচাই করো; 401/403, 400/422, 409/412 কোথাও গুলিয়েছে কি না বলো। ৩. টাকা দেওয়ার POST-এ timeout হলে কী ঘটে, ধাপে ধাপে লেখো; idempotency key কোথায় জমা, scope কী, ভিন্ন body বা চলমান অনুরোধে কী উত্তর — না থাকলে প্রস্তাব দাও। ৪. দুজন একসাথে একই বুকিং বদলালে lost update হয় কি না দেখাও। ৫. কোনো RFC নম্বর, header বা পণ্যের সীমা বললে উৎস দাও; নিশ্চিত না হলে স্পষ্ট লেখো। নতুন কোনো প্রযুক্তি যোগের আগে সরলতর বিকল্পটি বলো।

AI-এর API খসড়া গ্রহণের আগে — চেকলিস্ট

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

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

প্র ০১Fielding-এর REST আর আজকের “REST API” কি একই জিনিস? HATEOAS বাস্তবে কেন প্রায় হারিয়ে গেল?

একই নয়, আর পার্থক্যটা বোঝা জরুরি। Fielding ২০০০ সালে একটি ব্যাখ্যা খুঁজছিলেন: লাখো সার্ভার, কোটি পাতা, তবু একটিমাত্র ব্রাউজার সব জায়গায় চলে কীভাবে? তাঁর উত্তর ছিল uniform interface আর hypermedia. ব্রাউজার আগে থেকে কোনো সাইটের URL জানে না; পাতার link আর form ধরে এগোয়। তাই একটি সাইট নিজের URL-এর ছক বদলালেও ব্রাউজার ভাঙে না — server আর client স্বাধীনভাবে বদলাতে পারে।

২০০০-এর দশকে XML-ভিত্তিক ভারী SOAP আর WS-*-এর বিপরীতে “REST” হয়ে উঠল সরলতার প্রতীক — URL, HTTP method আর JSON. এই যাত্রায় hypermedia প্রায় হারিয়ে যায়। কারণটি গভীর: ব্রাউজারের পেছনে একজন মানুষ থাকেন, যিনি নতুন বোতাম দেখে বোঝেন কী করতে হবে। Mobile app-এর কোড অচেনা link-এর অর্থ নিজে বোঝে না; “pay” link মানে কী, তা আগেই কোডে লিখে রাখতে হয়। ফলে hypermedia-র বড় সুবিধা কমে যায়, অথচ খরচ — বড় উত্তর, জটিল client — থেকেই যায়।

তবু ধারণাটি মরে যায়নি। নতুন resource-এর Location header, pagination-এর next link, HAL বা JSON:API-এর মতো ছাঁচ — সবই hypermedia-র টুকরো। অবস্থাভিত্তিক কাজে এটি সত্যিই কাজে লাগে: “held” বুকিংয়ের উত্তরে pay আর cancel-এর link, “confirmed”-এ শুধু cancel — app link দেখেই ঠিক বোতাম দেখায়, ব্যবসার নিয়ম দুই জায়গায় লিখতে হয় না। Fielding নিজে ২০০৮ সালে ক্ষোভ প্রকাশ করেছিলেন যে hypertext-ছাড়া API-কেও REST বলা হচ্ছে; Richardson-এর মাপকাঠির স্তর ২ আর স্তর ৩-এর পার্থক্য ঠিক এটাই।

মূল উপলব্ধি: নামের শুদ্ধতা নিয়ে তর্কের চেয়ে জরুরি হলো জানা — কোন শর্ত কী সুবিধা দেয়, আর কোনটি আপনি ইচ্ছা করে ছেড়ে দিচ্ছেন। ইন্টারভিউতে “স্তর ২-এর REST-ধাঁচের API, link শুধু Location আর cursor-এ” বলতে পারা অনেক বেশি পরিণত উত্তর।

প্র ০২Offset pagination এত প্রচলিত কেন, আর বড় সার্চ বা feed সিস্টেম গভীর পাতা কীভাবে সামলায়?

Offset জনপ্রিয় কারণ এটি স্বাভাবিক: SQL-এর LIMIT … OFFSET-এর সাথে সরাসরি মেলে, “১ ২ ৩ … ৫০” পাতার সারি বানানো যায়, যেকোনো পাতায় লাফ দেওয়া যায়। ছোট তালিকায় দাম চোখেই পড়ে না।

দামটি বোঝা যায় হিসাবে। ধরা যাক ১০ লাখ সারি, প্রতি পাতায় ১০০ — মোট ১০,০০০ পাতা। পাতা k আনতে ডেটাবেস প্রায় k × ১০০ সারি পেরোয়। সব পাতা একবার করে পড়লে মোট পেরোনো সারি ১০০ × (১ + ২ + … + ১০,০০০) = ১০০ × ৫,০০,০৫,০০০ ≈ ৫০০ কোটি — অথচ সারি মাত্র ১০ লাখ, প্রায় ৫,০০০ গুণ বাড়তি কাজ। Keyset-এ প্রতিটি সারি মোটামুটি একবারই ছোঁয়া হয়। কাজটি পাতার সংখ্যার বর্গের হারে বাড়ে — তালিকা দ্বিগুণ হলে offset-এর মোট খরচ প্রায় চার গুণ।

বড় সিস্টেম তাই দুই কৌশল নেয়। প্রথমত, গভীরতার সীমা: Elasticsearch ডিফল্টভাবে from + size দিয়ে প্রায় ১০,০০০-এর বেশি গভীরে যেতে দেয় না (index.max_result_window; লেখার সময়ের ডিফল্ট, সংস্করণভেদে আলাদা), আর গভীরে যেতে সুপারিশ করে search_after — যা আসলে keyset. মানুষ সাধারণত প্রথম কয়েক পাতার পরে আর এগোন না, তাই সীমাটি ব্যবহারকারীর ক্ষতি করে না। দ্বিতীয়ত, feed-এ cursor: সামাজিক মাধ্যমের timeline বা chat-এর পুরোনো বার্তা — যেখানে নতুন জিনিস অনবরত মাথায় যোগ হয় — সেখানে infinite scroll আর cursor স্বাভাবিকভাবে মেলে।

সতর্কতা: cursor-ও পুরো তালিকার স্থির ছবি (snapshot) নয় — পড়ার মাঝে কোনো সারির sort key বদলালে সেটি সরে যেতে পারে; কড়া snapshot-এর নিজের খরচ আছে (MVCC, পর্ব ২৪)।

মূল উপলব্ধি: offset অবস্থান গোনে, cursor জিনিস চেনে। তালিকা ছোট আর স্থির হলে offset চলে; বড় বা চলমান হলে cursor, আর দুই ক্ষেত্রেই গভীরতার একটি সীমা।

প্র ০৩Body একই হলে server নিজেই কেন বুঝে নেয় না যে এটি retry? Idempotency key কেন client বানায়, আর “ঠিক একবার” কি আদৌ সম্ভব?

Body দেখে চেনা যায় না, কারণ একই body দুটি আলাদা অভিপ্রায়ও হতে পারে। একজন বাবা সকালে নিজের জন্য ঢাকা-রাজশাহীর ৮৫০ টাকার টিকিট কাটলেন, দুপুরে ছেলের জন্য একই ট্রিপে আরেকটি — body প্রায় অভিন্ন, দুটিই বৈধ। Retry আর নতুন কেনার পার্থক্য জানে শুধু client; তাই key বানায় client, প্রতিটি অভিপ্রায়ে একবার।

গভীরে আছে distributed system-এর একটি পুরোনো অসম্ভবতা — Two Generals’ Problem. ১৯৭০-এর দশকে (প্রায় ১৯৭৫ সালে Akkoyunlu, Ekanadham ও Huber-এর একটি লেখায়, আর পরে Jim Gray-এর লেখায় বর্তমান নামে) বর্ণিত গল্পটি এরকম: দুই সেনাপতি শত্রুর উপত্যকার দুই পাশে, একসাথে আক্রমণ করতে হবে, বার্তাবাহক ধরা পড়তে পারে। প্রথমজন বার্তা পাঠালেন; দ্বিতীয়জন প্রাপ্তিস্বীকার পাঠালেন — কিন্তু সেটি পৌঁছাল কি না, তিনি জানেন না; তার স্বীকারোক্তির স্বীকারোক্তি… শেষ বার্তাটি নিয়ে সন্দেহ কখনো মেটে না। HTTP-তেও তাই: উত্তর না পেলে client কখনো নিশ্চিত হতে পারে না যে server কাজটি করেছে কি না।

তাই অনির্ভরযোগ্য নেটওয়ার্কে “ঠিক একবার পৌঁছানো” সম্ভব নয়। যা সম্ভব: কমপক্ষে একবার পাঠানো (retry দিয়ে) আর idempotent প্রক্রিয়াকরণ (key দিয়ে) — ফলাফল কার্যত একবার। এর সীমাও আছে: key-এর মেয়াদ পেরোলে পুরোনো retry নতুন অনুরোধ হয়ে যায়; key দাবির পর server crash করলে “চলছে” অবস্থা আটকে থাকতে পারে, তাই সময়সীমা আর পুনরুদ্ধারের পথ লাগে; আর বাইরের gateway-কেও একই key দিতে হয়। শেষ রক্ষা হিসেবে অনেক পেমেন্ট ব্যবস্থা দিন শেষে নিজের খাতা gateway-র হিসাবের সাথে মেলায় (reconciliation).

মূল উপলব্ধি: নেটওয়ার্ক “ঠিক একবার” দিতে পারে না; সেটি বানাতে হয় প্রাপকের দিকে — অভিপ্রায়ের পরিচয় (key), জমা উত্তর আর পুরো শিকলে idempotency দিয়ে। পর্ব ৩৪-এ বার্তা-queue-এর প্রসঙ্গে একই সত্য আবার দেখবেন।

প্র ০৪একটি “নিরীহ” পরিবর্তনও কেন client ভাঙে? Hyrum’s law আর Postel-এর নীতি versioning সম্পর্কে কী শেখায়?

Google-এর প্রকৌশলী Hyrum Wright-এর নামে পরিচিত একটি পর্যবেক্ষণ আছে — Hyrum’s law, যা Software Engineering at Google (২০২০) বইয়ে বিস্তারিত এসেছে: API-র ব্যবহারকারী যথেষ্ট বেশি হলে চুক্তিতে যা-ই লেখা থাকুক, সিস্টেমের প্রতিটি চোখে পড়া আচরণের ওপর কেউ না কেউ নির্ভর করবে। উদাহরণ: JSON-এ ঘরের ক্রম বদলালেন — চুক্তিতে ক্রমের কোনো প্রতিশ্রুতি ছিল না, তবু কারও হাতে-লেখা parser ক্রম ধরে পড়ছিল। ত্রুটির বার্তার ভাষা একটু বদলালেন — কেউ সেই লেখা মিলিয়ে সিদ্ধান্ত নিচ্ছিল। উত্তর দ্রুততর করলেন — কারও কোডে লুকানো একটি race condition হঠাৎ দেখা দিল।

উল্টো দিকে আছে Jon Postel-এর robustness principle, ১৯৮০-র দিকের TCP-র মানদণ্ডে লেখা: “পাঠানোয় রক্ষণশীল, গ্রহণে উদার হোন।” API-তে এর ভালো রূপ হলো tolerant reader — client অচেনা ঘর উপেক্ষা করবে। কিন্তু অতি উদার গ্রহণের সমালোচনাও আছে: server যদি ভুল-ভাল অনুরোধও মেনে নেয়, ভুলগুলো ছড়িয়ে স্থায়ী হয়ে যায়, আর পরে কড়া হলেই সবাই ভাঙে। IETF-এর RFC 9413 (২০২৩) এই অভিজ্ঞতা থেকে protocol-কে সময়ের সাথে সক্রিয়ভাবে রক্ষণাবেক্ষণের কথা বলে।

তাহলে versioning কী দেয়? Version নম্বর আসলে যোগাযোগের হাতিয়ার — “এখানে ভাঙা পরিবর্তন আছে।” আসল সুরক্ষা অন্য জায়গায়: শুরু থেকেই কড়া validation (পরে কড়া করা মানে ভাঙা); চুক্তিতে স্পষ্ট লেখা যে নতুন ঘর আর enum মান আসতে পারে; OpenAPI-র দুই সংস্করণের স্বয়ংক্রিয় তুলনা; আর client id ধরে কে কোন সংস্করণ ও ঘর ব্যবহার করছে তা মাপা — তাহলে নির্ভরতা অনুমানে নয়, তথ্যে জানা যায়।

মূল উপলব্ধি: চুক্তি শুধু লেখা নয়, আচরণও। পরিবর্তন ছোট রাখুন, মেপে দেখুন, আগে জানান — version নম্বর এই শৃঙ্খলার বিকল্প নয়।

প্র ০৫Status code নিয়ে এত তর্ক কেন? GraphQL বা JSON-RPC তো প্রায়ই সব উত্তরে 200 দেয় — তাতে সমস্যা কোথায়?

Status code-এর ইতিহাসেই তর্কের বীজ। 422 এসেছিল HTTP-র মূল মানদণ্ড থেকে নয়, WebDAV-এর জন্য (RFC 4918, ২০০৭); বহু বছর পরে RFC 9110 (২০২২) একে মূল HTTP-তে আনে “Unprocessable Content” নামে। 429 যোগ হয় RFC 6585-এ (২০১২), আরও কয়েকটি কোডের সাথে, যেমন 428. আর 418 “I'm a teapot” এসেছিল ১৯৯৮ সালের পয়লা এপ্রিলের একটি রসিকতার RFC (2324) থেকে — মনে করিয়ে দেয়, প্রতিটি কোড ব্যবহারের জন্য বানানো নয়। কোডগুলো সময়ের সাথে জমেছে, তাই একই পরিস্থিতির জন্য প্রায়ই দুটি যুক্তিসংগত উত্তর থাকে — 400 না 422, 403 না 404.

তবু কোড গুরুত্বপূর্ণ, কারণ মাঝের যন্ত্রগুলো কোড দেখেই কাজ করে। Retry-র লাইব্রেরি 503 পেলে আবার চেষ্টা করে, 400 পেলে করে না; monitoring ৫xx-এর হার দেখে সতর্কসংকেত বাজায়; load balancer বারবার 5xx দেওয়া সার্ভারকে সরিয়ে দিতে পারে। তাই validation-এর ভুলে 500 দিলে রাতে অকারণে কারও ঘুম ভাঙে; আসল বিভ্রাটে 200 দিলে dashboard সবুজ, অথচ যাত্রীরা টিকিট পাচ্ছেন না।

GraphQL আর JSON-RPC HTTP-কে মূলত একটি সুড়ঙ্গ হিসেবে ব্যবহার করে: একটি endpoint, আর প্রচলিত ব্যবহারে ত্রুটি থাকে body-র errors তালিকায়, HTTP status প্রায়ই 200 (নতুন নির্দেশিকা কিছু ক্ষেত্রে ভিন্ন কোড সুপারিশ করে)। এর পেছনে যুক্তি আছে — একটি GraphQL অনুরোধ অনেক ঘর চায়, কিছু পাওয়া যায়, কিছু যায় না; আংশিক সফলতা একটি কোডে প্রকাশ করা যায় না। দাম হলো HTTP-স্তরের caching, monitoring আর retry অন্ধ হয়ে যায়; দলকে নিজের ত্রুটি-কোড আর metric বানাতে হয় (পর্ব ০৯)।

মূল উপলব্ধি: শুদ্ধতার চেয়ে সামঞ্জস্য বড়। একটি রীতি বেছে চুক্তিতে লিখুন, সব endpoint-এ মানুন, আর মনে রাখুন — কোডটি মানুষের জন্য নয়, পথের প্রতিটি যন্ত্রের জন্য।

অনুশীলন

আগে নিজে খাতায় লিখুন, তারপর উত্তরের সাথে মেলান। নকশার প্রশ্নে একটিমাত্র সঠিক উত্তর নেই — যুক্তিটাই আসল।

  1. একটি ফিচারের endpoint নকশা — অপেক্ষমাণ তালিকা: বাস-টিকিট সেবায় সিট না থাকলে যাত্রী একটি ট্রিপের waitlist-এ নাম লেখাবেন। কেউ বাতিল করলে তালিকার প্রথমজন ১৫ মিনিটের জন্য সিটের প্রস্তাব পাবেন; গ্রহণ করলে বুকিং হবে। যাত্রী নিজের অবস্থান দেখবেন, চাইলে নাম তুলে নেবেন; কাউন্টারের কর্মী পুরো তালিকা দেখবেন। Endpoint, status code, pagination আর নিরাপত্তার নিয়ম লিখুন।
    POST   /v1/trips/{trip_id}/waitlist-entries        201 + Location | 409 already listed | 422 trip departed
    GET    /v1/waitlist-entries/{entry_id}             200 { position, status, offer_expires_at } | 404
    DELETE /v1/waitlist-entries/{entry_id}             204 | 404
    POST   /v1/waitlist-entries/{entry_id}/acceptance  201 booking (Idempotency-Key) | 409 offer expired
    GET    /v1/trips/{trip_id}/waitlist-entries?limit=50&cursor=…   staff only: 200 | 403
    • Resource আর nesting: নাম লেখানো আর পুরো তালিকা ট্রিপের অধীনে (এক স্তর); একটি entry তার id দিয়ে সরাসরি।
    • গ্রহণ মানে বুকিং তৈরি: তাই POST আর বাধ্যতামূলক idempotency key; মেয়াদ পেরোলে 409 — অনুরোধ ঠিক, কিন্তু বর্তমান অবস্থার সাথে সংঘাত।
    • ক্রম আর pagination: ন্যায্যতার জন্য (created_at, entry_id) ক্রম; কর্মীর তালিকায় cursor, কারণ মাঝে নাম যোগ হয় আর ওঠে।
    • Cache আর নিরাপত্তা: অবস্থান ব্যক্তিগত ও দ্রুত বদলায় — private, no-cache আর ETag; entry-টি এই যাত্রীরই কি না প্রতিবার যাচাই, না হলে 404; একটি ট্রিপে একজন একবারই (UNIQUE (trip_id, passenger_id)).
    • প্রস্তাবের খবর: push বা SMS-এ (পর্ব ৫৭); মেয়াদ শেষে পরের জনকে প্রস্তাব দেওয়া পেছনের job-এর কাজ (পর্ব ৩৭)।
  2. Status code বাছাই: প্রতিটির জন্য কোড আর এক লাইনের কারণ লিখুন — (ক) একই বুকিং দ্বিতীয়বার DELETE; (খ) একই Idempotency-Key-এর প্রথম অনুরোধ এখনো চলছে, এমন সময় retry এলো; (গ) যে API-তে If-Match বাধ্যতামূলক, সেখানে If-Match ছাড়া PATCH; (ঘ) JSON ঠিক, কিন্তু তারিখ 2026-02-31; (ঙ) nginx পেছনের অ্যাপ সার্ভারে সংযোগই পেল না; (চ) রক্ষণাবেক্ষণ চলছে, ১০ মিনিট পরে ফিরবে; (ছ) বাতিলের অনুরোধ নেওয়া হলো, টাকা ফেরত পরে; (জ) টোকেন ঠিক, কিন্তু যাত্রী অন্য একজনের বুকিং খুলতে চাইলেন।
    • (ক) 404 (কিছু API-র নীতিতে 204) — উত্তর আলাদা, কিন্তু অবস্থা একই: বুকিংটি নেই।
    • (খ) 409 — চলমান কাজের সাথে সংঘাত; client একটু পরে একই key-তে আবার চেষ্টা করবে।
    • (গ) 428 Precondition Required — 412 নয়, কারণ শর্ত দেওয়াই হয়নি।
    • (ঘ) 422 — গঠন ঠিক, অর্থে ভুল; কিছু দলের রীতিতে 400.
    • (ঙ) 502 Bad Gateway — সংযোগ পেয়ে সময়মতো উত্তর না পেলে হতো 504.
    • (চ) 503 আর Retry-After: 600.
    • (ছ) 202 Accepted, সাথে Location-এ ফেরতের অবস্থা দেখার ঠিকানা।
    • (জ) 404 — 403 দিলে জানিয়ে দেওয়া হয় যে বুকিংটি আছে; অস্তিত্ব লুকানোই নিরাপদ।
  3. ভাঙা pagination সারানো (হিসাবসহ): একজন সহকর্মী রাতের sync job-এর জন্য দিলেন: GET /v1/bookings?page=N&size=1000, ক্রম শুধু ORDER BY departure_at, আর প্রতিটি উত্তরে নতুন করে গোনা total_count. টেবিলে ৫০ লাখ বুকিং, আর job চলার সময়েও নতুন বুকিং ও বাতিল চলতে থাকে। (ক) সমস্যাগুলো কী? (খ) পুরো তালিকা একবার পড়তে offset-এ ডেটাবেসকে মোট কত সারি পেরোতে হয় (পাতা k-তে offset + size সারি ধরে), আর keyset-এ মোটামুটি কত? (গ) নতুন নকশা লিখুন।

    (ক) সমস্যা পাঁচটি: গভীর পাতা ধীর; পড়ার মাঝে যোগ-মোছায় সারি দুবার আসে বা হারায় — sync-এর জন্য মারাত্মক; departure_at অনন্য নয়, তাই একই সময়ের সারির ক্রম প্রতিবার ভিন্ন হতে পারে; প্রতি পাতায় COUNT(*) মানে ৫,০০০ বার পুরো টেবিল গোনা; আর মুছে ফেলা বুকিং sync জানতেই পারে না।

    (খ) হিসাব। পাতা = ৫০,০০,০০০ ÷ ১,০০০ = ৫,০০০. পাতা k-তে (k = ০ থেকে ৪,৯৯৯) পেরোনো সারি k × ১,০০০ + ১,০০০, তাই মোট = ১,০০০ × (৫,০০০ × ৫,০০১ ÷ ২) = ১২,৫০,২৫,০০,০০০ ≈ ১.২৫ × ১০১০; শেষ পাতাতেই ৫০ লাখ। Keyset-এ প্রতিটি সারি মোটামুটি একবার — প্রায় ৫০ লাখ, সাথে ৫,০০০টি ছোট index-খোঁজ; পার্থক্য প্রায় ২,৫০০ গুণ।

    GET /v1/bookings?updated_after=2026-11-01T00:00:00Z&limit=500&cursor=…
    -> ORDER BY updated_at, id      index (updated_at, id)
    200 { "items": [ … ], "next_cursor": "…", "has_more": true }
        cancelled bookings stay as rows with "status": "cancelled" (soft delete)

    (গ) নকশা: অনন্য ক্রম (updated_at, id) আর সেই index; অস্বচ্ছ cursor; পাতা সর্বোচ্চ ৫০০; total_count-এর বদলে has_more; প্রতি রাতে শুধু আগের রাতের পর বদলানোগুলো; আর বাতিল মুছে না ফেলে status বদলানো, যাতে sync বাতিলও দেখে।

  4. Output আন্দাজ করুন: অংশ ৭-এর idempotency_sim.js-এ const pay আর show-এর পরের লাইনগুলো মুছে নিচের ছয় লাইন বসালেন (store খালি, ওয়ালেট ৫,০০০, পরের payment id 77). প্রতিটি লাইনের উত্তর আর ওয়ালেট কী হবে? কোন লাইনটি অবাক করবে, আর কেন?
    show("a POST K9", handle("K9", pay));
    show("b retry K9", handle("K9", pay));
    show("c K9, keys reordered", handle("K9", { amount: 850, booking: "bk_1001" }));
    show("d POST K10 (slow)", handle("K10", pay, true));
    show("e retry K10", handle("K10", pay));
    show("f server finishes K10", finish("K10", pay));
    a POST K9                     201 pay_77 charged 850                wallet 4150
    b retry K9                    201 pay_77 charged 850  (replayed)    wallet 4150
    c K9, keys reordered          422 key reused with a different body  wallet 4150
    d POST K10 (slow)             (still working)                       wallet 4150
    e retry K10                   409 first attempt still running       wallet 4150
    f server finishes K10         201 pay_78 charged 850                wallet 3300

    অবাক করার লাইন c: অর্থ হুবহু এক — একই বুকিং, একই ৮৫০ — শুধু ঘরের ক্রম উল্টো, তবু 422. কারণ fingerprint বানানো হয়েছে JSON.stringify-এর লেখার ওপর, আর ক্রম বদলালে লেখা বদলায়, hash-ও বদলায়। বাস্তবে ভিন্ন লাইব্রেরি বা app-এর ভিন্ন সংস্করণ একই অনুরোধ ভিন্ন ক্রমে লিখতেই পারে। সমাধান: hash-এর আগে JSON-কে একটি প্রমিত রূপে আনা (ঘরগুলো নামের ক্রমে সাজানো, ফাঁকা জায়গা বাদ), অথবা parse করা নির্দিষ্ট ঘরগুলোর ওপর hash. বাকিগুলো প্রত্যাশিত: d-তে কাজ চলছে বলে টাকা কাটেনি, e-তে 409, আর f-এ K10-এর জন্য একবারই কাটা — নতুন key বলে এটি নতুন payment, pay_78.

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

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

ক্রমচালু একটি API-তে breaking change আনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. নতুন বড় সংস্করণ (যেমন /v2) পুরোনোটির পাশাপাশি চালু করা
  2. ডকুমেন্টেশন আর পুরোনো থেকে নতুনে যাওয়ার নির্দেশিকা প্রকাশ
  3. পুরোনো সংস্করণের উত্তরে Deprecation আর Sunset header যোগ
  4. client id ধরে পুরোনো সংস্করণের ব্যবহার মাপা আর শেষের দিকের ব্যবহারকারীদের সাথে যোগাযোগ
  5. Sunset তারিখের পর পুরোনো সংস্করণ বন্ধ, উত্তরে 410 Gone

নিচের কোন HTTP method idempotent, কিন্তু safe নয়?

  • GET
  • POST
  • PUT
  • PATCH

একটি API সফলভাবে নতুন একটি বুকিং তৈরি করল, যার id bk_1002. প্রচলিত REST রীতিতে সবচেয়ে উপযুক্ত উত্তর কোনটি?

  • 200 OK, body-তে id
  • 201 Created আর Location: /v1/bookings/bk_1002
  • 202 Accepted
  • 204 No Content

একজন client একটি বুকিং পড়ে ETag "v7" পেলেন। পরে If-Match: "v7" দিয়ে PUT পাঠালেন, কিন্তু মাঝে অন্য কেউ বুকিংটি বদলেছে এবং ETag এখন "v8". Server-এর উত্তর কী হওয়া উচিত?

  • 200 OK, নতুন রূপ বসিয়ে দেওয়া
  • 304 Not Modified
  • 409 Conflict
  • 412 Precondition Failed

একটি তালিকা নতুন-আগে ক্রমে, প্রতি পাতায় ১০টি, offset দিয়ে পড়া হচ্ছে। পাতা ১ পড়ার পর তালিকার মাথায় ২টি নতুন সারি যোগ হলো। এবার offset=10 দিয়ে পাতা ২ চাইলে কী দেখা যাবে?

  • পাতা ১-এর শেষ ২টি সারি আবার আসবে
  • নতুন ২টি সারি পাতা ২-এর শুরুতে আসবে
  • ২টি সারি বাদ পড়বে, কখনো দেখা যাবে না
  • কিছুই বদলাবে না

IETF-এর Idempotency-Key খসড়া অনুযায়ী, প্রথম অনুরোধ শেষ হওয়ার পর একই key আগের চেয়ে ভিন্ন body নিয়ে এলে server কী উত্তর দেবে?

  • আগের জমা উত্তর (যেমন 201) হুবহু
  • 422 Unprocessable Content
  • 409 Conflict
  • 500 Internal Server Error

চালু একটি API-তে নিচের কোন পরিবর্তনটি সাধারণত পুরোনো client-এর জন্য নিরাপদ (backward compatible)?

  • fare ঘরের একক টাকা থেকে পয়সায় বদলানো
  • seat_no ঘরের নাম বদলে seat করা
  • উত্তরে নতুন একটি ঐচ্ছিক ঘর bus_type যোগ করা
  • ঐচ্ছিক phone parameter-কে বাধ্যতামূলক করা

শব্দকোষ

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

মনে রাখুন

  1. URI-তে বিশেষ্য, কাজ বলে method; GET কখনো কিছু বদলায় না; PUT আর DELETE idempotent, POST নয়।
  2. প্রতিটি পরিস্থিতির জন্য ঠিক status code — 201 আর Location, 202, 409, 412, 422, 429 আর Retry-After — আর ত্রুটি এক ছাঁচে (problem+json).
  3. বড় তালিকায় cursor: অনন্য sort key, index আর অস্বচ্ছ টোকেন; offset গভীরে ধীর আর পড়ার মাঝের পরিবর্তনে ভুল দেখায়।
  4. যোগ করুন, ভাঙবেন না; ভাঙতেই হলে নতুন সংস্করণ, Deprecation আর Sunset, আর ব্যবহার মেপে অবসর।
  5. POST-এর retry নিরাপদ করে idempotency key, একসাথে সম্পাদনা নিরাপদ করে ETag আর If-Match; দীর্ঘ কাজে 202 আর অবস্থা-resource.

পরের পর্বে: পর্ব ০৯ — gRPC আর GraphQL: Protocol Buffers, streaming আর schema-ভিত্তিক API. সার্ভিসের ভেতরের দ্রুত যোগাযোগে কেন JSON-এর বদলে binary, আর একটি অনুরোধে ঠিক যতটুকু দরকার ততটুকু চাওয়ার দাম কী — REST-এর পাশে রেখে দেখব।

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

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