DEV Community

Monirul Islam
Monirul Islam

Posted on Originally published at mislam-dev.vercel.app

Day 3 — REST API Design — যেভাবে করলে কেউ কিছু বলতে পারবে না

আগে যখন কোনো web project develop করা হতো তখন typically server total content generate করে frontend-এ send করে দিত, then frontend এটা render করে display করতো। এখন একটা বিষয় চিন্তা করে দেখুন, একটা full page যদি প্রতিবার server থেকে load হয়, তাহলে network latency + bandwidth খরচ কিন্তু বেশি হবে। মোটামুটি সব user-ই কিন্তু almost similar জিনিস বারবার server থেকে load করবে। এখানে এই content-এর ভেতরে শুধুমাত্র user specific information-গুলোই change হবে, বাকিগুলো similar। তখন ইঞ্জিনিয়াররা ভাবলো, আমরা যদি এই fixed data-গুলো frontend-এই রেখে, শুধু user specific data-গুলোই server থেকে fetch করে ঐ যেখানে যেখানে লাগবে সেখানে বসিয়ে দিই তাহলে কেমন হবে? এতে করে cost reduce হয়ে যাবে + network latency-ও কমে যাবে।

এভাবেই মূলত API-এর শুরু হয়। API-এর মূল purpose হচ্ছে client-server communication হবে only necessary data-এর মাধ্যমে। No unnecessary data।


REST API কী?

Suppose, আপনি একটা ৫ তলা ঘর বানাচ্ছেন। এখন আপনি কি যেমন তেমনভাবে বানাবেন?
অবশ্যই না। আপনি একজন engineer নিয়ে এসে একটা design করে নেবেন। আপনাকে proper একটা architecture দিয়ে দেবে এবং সেই architecture-এ আপনি বাড়িটা বানাবেন।

Similar way-তে REST হলো API বানানোর একটা architecture style। REST-এর full form হচ্ছে Representational State Transfer। আর মজার ব্যাপার হচ্ছে আপনি চাইলে যেকোনো architecture fully follow-ও করতে পারেন আবার partially-ও use করতে পারেন। এর জন্য এই REST-কে rules বলার থেকে guidelines বলাটাই better।

REST-এর মূল concept হচ্ছে, সবকিছু resource। আর এই resource-এর উপরে HTTP method দিয়ে request করলেই হয়ে যাবে। এইটুকু করলেই হয়ে যাবে। HTTP নিয়ে তো already জানেন।


REST API কেন ডিজাইন করাটা জরুরি?

এটা একদম easy। তাহলে এটা নিয়ে এত ভাবার কী আছে? একটা জিনিস মাথায় রাখবেন, যে জিনিস যত flexible সেখানে problem-এর সংখ্যা তত বেশি। Initially আপনি simple way-তে API develop করবেন তখন কোনো problem হবে না। কিন্তু যখন একটু grow করবেন তখন যে কী ধরনের problem হবে আপনি experience না করলে বুঝতে পারবেন না।

আমার একটা real life experience share করি তাহলে বুঝতে পারবেন।

আমি যখন আমার development journey শুরু করি, তখন present time-এর মতো API development বা API-এর demand ছিল না। Just server থেকে সবকিছু ready হয়ে আসবে, সবকিছুই আরামসে চলে যাবে। এত বেশি প্যারা নেওয়ার দরকার নেই।
আস্তে আস্তে আমি API development-এর দিকে move করি। এই journey-এর শুরু হয় tutorial follow করে করে, সাথে practice করতাম। এভাবেই আস্তে আস্তে used to হয়ে যাই। একটা সময় যেয়ে tutorial দেখার থেকে documentation পড়া শুরু করি।

অনেকগুলো project করার পরে একটা সময় যেয়ে আমি একটা decision নিই যে, আমি একটা বড় project বানাবো যেটা real life work-টা reduce করবে and believe me, সেই project এই নিয়ে ৪ বার try করেও সেটা complete করতে পারিনি। এর অনেকগুলো reason ছিল, তার মধ্যে API properly design না করা একটা।

এখন বর্তমান সময়ে আমি ঐ project open করলে আমি নিজেই সেই project বুঝতে পারি না। এখন একটা বিষয় চিন্তা করেন আমার নিজের লেখা code & style, নিজেই বুঝতে পারছি না। এটা যদি অন্য কেউ দেখে তার কী অবস্থা হবে? আপনার team feature development করার বদলে project style বুঝতে বুঝতেই অনেক সময় চলে যাবে। এই জন্য API properly design করাটা জরুরি।


Resource Naming: নাম রাখার নিয়ম

আগের article-এ আপনি HTTP method নিয়ে already জেনে গেছেন। এই method থেকে আপনি বুঝে যাবেন যে আসলে এটা server থেকে data fetch করা হচ্ছে নাকি server-এ send করা হচ্ছে। এই ক্ষেত্রে আপনার resource-এর নামের সাথে get বা post লেখার প্রয়োজন হবে না। Method is explaining everything।

Resource naming-এর ক্ষেত্রে noun use করা হয়। আর verb word (create, get, update, delete) গুলো avoid করা হয়। কারণ HTTP method-গুলো এগুলো explain করে দেয়। নিচে একটা example দেয়া হলো, এটা দেখলে বুঝতে পারবেন।

❌ খারাপ:

POST /createUser
GET /getUsers
DELETE /deleteUser/5
Enter fullscreen mode Exit fullscreen mode

✅ ভালো:

POST /users           # create new user
GET /users            # get all users
DELETE /users/5       # delete user with id 5
Enter fullscreen mode Exit fullscreen mode

Resource naming-এর ক্ষেত্রে plural use করা হয়। এটা একটা convention।


Nested Resources: কতটা গভীরে যাবেন?

URL path + resource naming আরও convenient করতে nesting resource use করা হয়। নিচের example-এর মতো:

GET /users/5/posts/3/comments
Enter fullscreen mode Exit fullscreen mode

কিন্তু কথা হচ্ছে এই nesting করলে তো URL অনেক বড় হয়ে যাবে এবং URL-এ unwanted data-এর প্রয়োজন হবে। For example: যখন আমার সবার জন্য post-গুলো দরকার হবে তখন কিন্তু users define করার কোনো প্রয়োজনীয়তা নেই। আবার same rules যায় single and specific resource-এর ক্ষেত্রে। একটা post-এর ID সম্পূর্ণ posts table-এ generally unique হয়। তাহলে post-এর ID দিয়ে direct access না করে এত nesting করে access করা make sense করে না।

এখন বলতে পারেন, এর জন্য কি nesting use করবো না? অবশ্যই use করবেন তবে maximum ২ level। নিচের example দেখলে বুঝতে পারবেন:

GET /users/5/posts       ✅
GET /posts/3/comments    ✅
GET /users/5/posts/3/comments/7/replies  ❌
Enter fullscreen mode Exit fullscreen mode

এটা একটা convention, আপনার যদি মনে হয় আপনি nesting level আরও increase করবেন, it's all up to you।


Versioning: ভবিষ্যতের জন্য দরজা খোলা রাখা

মনে করুন আপনি একটা application বানিয়েছেন, যেটা সময়ের সাথে সাথে অনেক grow করেছে। একটা সময় যেয়ে আপনার API architecture + request + response + logic এগুলো change করা দরকার। আর আপনার application এত বড় হয়েছে যে আপনি existing API change করতে পারবেন না। এটা কিন্তু একটা problem।
এই problem-টা solve করার solution হলো versioning। Versioning করার মাধ্যমে এই problem-টা solve করা যায় easily।

API-কে ৩ way-তে versioning করা যায়:

  • using headers: এই way-তে request header-এর ভেতরে version define করে দিতে হয়। এর মাধ্যমে server versioning-টা manage করে। এর benefit হচ্ছে আপনার URL same থাকে but function + internal logic change হয়ে যায়।
  • url path: এটায় URL path-এর ভেতরে version define করতে হয়।
  • url query parameter: এটায় URL-এ query parameter হিসেবে version-টা define করে দিতে হয়। এটা অনেকটা header versioning-এর মতো।

url versioning:

/api/v1/users
/api/v2/users
Enter fullscreen mode Exit fullscreen mode

Query versioning:

/users?version=1
/users?version=2
Enter fullscreen mode Exit fullscreen mode

তবে practically URL versioning-টাই সবচেয়ে বেশি use হয়। কারণ এখানে complexity অনেক কম। তবে বিষয়টা এমন না যে সবাই use করছে বলে আপনাকেও use করতে হবে।


Idempotency: এই concept টা না জানলে খবর আছে!

মনে করুন, আপনার একটা ছোট ভাই জিজ্ঞাসা করছে, বাংলাদেশের রাজধানী (capital) কোথায়? আপনি normally বললেন ঢাকা। কিছুক্ষণ পরে এসে আবার same question করেছে। আপনিও same answer দিলেন। এভাবে next ১০ মিনিটে ১০-১২ বার জিজ্ঞাসা করা হয়ে গেছে। এবার আপনি আপনার মেজাজ হারিয়ে ফেললেন এবং রেগে রেগে বললেন যে রাজধানী জিনিসটা কোনো ছেলেখেলা না যে এত তাড়াতাড়ি change হয়ে যাবে।
তো এই change না হওয়ার concept-টাই হচ্ছে Idempotency। API-এর ক্ষেত্রে আপনি একটা specific resource access করছেন। এখন ঐ resource-টা idempotent হতে হলে সবসময় ঐ same response provide করতে হবে। সেটা ১০০ বার হোক বা ২০০ বার। [তবে এর ভেতরে data update করলে বিষয়টা আলাদা জিনিস]। Concept-টা hopefully clear হয়ে গেছে।

এখন চলুন HTTP method-এর idempotency নিয়ে কিছু আলোচনা করি:

  • POST: স্বাভাবিক কারণেই POST method idempotent করা possible না কারণ per request-এ এটা নতুন data return করবে। তবে এটাকেও idempotent করা সম্ভব। modern system-এ request এর header এ key provide করা হয়। সেই key থেকে server বুঝতে পারে যে এটা duplicate ডাটা কিনা।
  • GET - এটা idempotent হতেই হবে। না হলে duplicate data নিয়ে অনেক প্যারা খেতে পারেন।
  • PUT/PATCH - এখানে একটু confusing লাগতে পারে। এখানে ensure করতে হবে যেই resource-টা update করছি, always যেন সেই resource-টাই update হয়। তাহলে এটাকে idempotent বলা যাবে।
  • DELETE - এখানে concept-টা PUT method-এর মতো। তবে same ID দিয়ে first request-এ actual data delete হবে কিন্তু বাকি সব ক্ষেত্রে তা Not Found বা Already Gone show করবে।

এই idempotency-এর concept-টা API + আপনার application-কে অনেকটা stable এবং predictable করবে। এটা না জেনে যদি আপনি API develop করেন তবে অনেক unexpected problem face করতে পারেন।


Pagination Strategies

আপনার কাছে মিলিয়ন মিলিয়ন ডাটা আছে। এই ডাটাকে একবারে ফেচ করলে লেটেন্সি অনেক বেড়ে যাবে। আর ইউজারের পক্ষে এত ডাটা একবারে রিড করা সম্ভব না। তাই ইউজার এক্সপেরিয়েন্স প্রপারলি মেইনটেইন করার জন্য চাঙ্ক চাঙ্ক (টুকরো টুকরো) করে ডাটা রেসপন্স করাটা বেটার। এই চাঙ্ক চাঙ্ক আকারে ডাটা রেসপন্স করার সিস্টেমকেই বলে Pagination।

বিষয়টা এমন হবে যে—ইউজার বলবে আমাকে ১০টা করে আইটেম (limit) দাও এবং ১ম (page) ১০টা আইটেম দাও। তখন সার্ভার "১ম ১০টি" আইটেম প্রোভাইড করবে। এটা একটা সিম্পল স্ট্র্যাটেজি। সার্ভার এখানে সব ডাটাকে limit দিয়ে ভাগ করবে, তারপর যে পেজের ডাটা ইউজার চাইবে সেই পেজের ডাটা ইউজারকে প্রোভাইড করবে।

Pagination-এর জন্য বেশ কিছু স্ট্র্যাটেজি আছে।

Offset

এই স্ট্র্যাটেজি একদম সিম্পল—ইউজারের কাছ থেকে limit এবং offset (কয়টা আইটেম বাদ দিতে হবে) ইনপুট নেওয়া হয়। তারপর রেসপন্স করা হয়ে থাকে।

Example:

/api/v1/users?limit=10&offset=20
Enter fullscreen mode Exit fullscreen mode

এই স্ট্র্যাটেজি সিম্পল কিন্তু বিশাল ডেটাসেটের ক্ষেত্রে বটলনেক তৈরি করতে পারে। এখানে যে offset আছে, এই offset কিন্তু ডাটাবেসে কুয়েরি করার সময় রিড হয়। এখন যদি ডাটাবেসে ১ মিলিয়ন ডাটা থাকে এবং offset-এর ভ্যালু হাফ মিলিয়নে চলে যায়, তখন কিন্তু এই হাফ মিলিয়ন ডাটাই রিড করতে হবে।
এখন আপনার পেজিনেশনের যে উদ্দেশ্য, সেটা কিন্তু অ্যাচিভ হচ্ছে না। ইনিশিয়ালি এটা ভালো মনে হলেও ডেটাসেট বিশাল হলে প্রবলেম শুরু হয়ে যায়।

Cursor

এই স্ট্র্যাটেজিতে একটা cursor (unique id, timestamp ইত্যাদি) ব্যবহার করা হয়। লাস্ট যে ডাটার cursor ভ্যালু হিসেবে প্রোভাইড করা হবে, তারপর থেকে ডাটা ফেচ করা হয়।

/api/v1/users?limit=10&cursor=123e4567-e89b-12d3-a456-426614174000
Enter fullscreen mode Exit fullscreen mode

এই স্ট্র্যাটেজি অফসেটের সমস্যা সমাধান করে। কিন্তু এই স্ট্র্যাটেজি ইমপ্লিমেন্ট করা অফসেটের চেয়ে অনেকটাই কমপ্লেক্স। এ ছাড়াও এই স্ট্র্যাটেজিতে ডুপ্লিকেট ডাটা রিটার্ন হওয়ার পসিবিলিটি থাকে। এই স্ট্র্যাটেজিতে কেয়ারফুলি হ্যান্ডেল না করলে এই ইস্যু ফেস করতে হতে পারে।

Cursor Based Pagination বিশাল ডেটাসেটের জন্য বেস্ট চয়েস। আর ইন্ডাস্ট্রিতে এই স্ট্র্যাটেজি সবচেয়ে বেশি ব্যবহার হয়।

Time-Based Pagination

এই স্ট্র্যাটেজিতে limit-এর পরিবর্তে একটা timestamp-এর রেঞ্জ প্রোভাইড করা হয়ে থাকে।

Example:

/api/v1/users?limit=10&from_time=1234567890&to_time=1234567890
Enter fullscreen mode Exit fullscreen mode

এই স্ট্র্যাটেজি সব ধরনের ডেটাসেটের সাথে সুইটেবল না। যেখানে ডাটাগুলো টাইমস্ট্যাম্পের ভেতরে থাকে, সেই ক্ষেত্রে এটা বেস্ট কাজ করে। যেমন: logs, analytics রিলেটেড ডাটা ইত্যাদি।

Final Thought

ওপরের সব স্ট্র্যাটেজিগুলো কিছু স্পেসিফিক প্রবলেম সলভ করে। তবে প্রোডাকশনে রিকোয়ারমেন্ট অনুযায়ী হাইব্রিড অ্যাপ্রোচ ব্যবহার করা হয়। কারণ এই স্ট্র্যাটেজিগুলো কিছু বেনিফিট দেয় + কিছু ওভারহেডও নিয়ে আসে। কোনো স্ট্র্যাটেজিই পারফেক্ট না। তবে ইন্ডাস্ট্রিতে সবচেয়ে বেশি ব্যবহার হয় cursor based pagination।


Error Response Strategy

একটা অ্যাপ্লিকেশনে সবকিছু নিখুঁতভাবে কাজ করবে না। ইউজার ইনভ্যালিড ডাটা ইনপুট দিতেই পারে। এখন ইউজার যে ইনভ্যালিড ডাটা দিয়েছে, কী ডাটা দিয়েছে, কী ভুল করেছে—এই সব ইনফরমেশন ইউজারকে প্রপারলি প্রোভাইড করতে হবে, এটাই স্বাভাবিক ব্যাপার।

এখন কোনো error তৈরি হলে, সেটা প্রপারলি হ্যান্ডেল করে ইউজারকে একটা গোছানো ফরম্যাটে error response প্রোভাইড করতে হবে। তবে এই error response যেন predictable হয়, সেটা নিশ্চিত করতে হবে।

Error response-এর জন্য RFC 9457 স্ট্যান্ডার্ড রয়েছে।

RFC 9457 ("Problem Details for HTTP APIs") হলো একটি স্ট্যান্ডার্ড স্পেসিফিকেশন, যা HTTP API-এর জন্য সহজে বোঝা যায় এমন JSON বা XML এরর ফরম্যাট নির্ধারণ করে।

এই স্ট্যান্ডার্ড অনুযায়ী কিছু ফিল্ড প্রোভাইড করতে হয়, তবে সব ফিল্ড দেওয়া বাধ্যতামূলক না। নিচে উদাহরণ দেওয়া হলো:

  • type (URI Reference): একটি লিঙ্ক বা URI যা স্পেসিফিক সমস্যার ধরনকে চিহ্নিত করে। আদর্শভাবে এটি এমন একটি ডকুমেন্টেশনের লিঙ্ক হওয়া উচিত যা দেখলে এরর সম্পর্কে বিস্তারিত বোঝা যায়। কিছু না দিলে এটি ডিফল্ট হিসেবে about:blank থাকে।
  • status (Number): এই সমস্যার জন্য সার্ভার থেকে জেনারেট হওয়া সঠিক HTTP status code (যেমন: 400, 404, 422)।
  • title (String): সমস্যার একটি সংক্ষিপ্ত ও সহজবোধ্য শিরোনাম। এটি প্রতিবার একই রকমের হওয়া উচিত এবং কেবল ভাষার পার্থক্যের জন্য পরিবর্তিত হতে পারে।
  • detail (String): এই নির্দিষ্ট এররের পেছনের কারণ নিয়ে বিস্তারিত ব্যাখ্যা। এটি একেক রিকোয়েস্টে একেক রকম হতে পারে এবং ডিবাগিংয়ে সাহায্য করে।
  • instance (URI Reference): একটি ইউনিক URI বা পাথ যা নির্দিষ্ট এই ঘটনাটিকে আইডেন্টিফাই করে। এটি সাধারণত ইন্টারনাল রিকোয়েস্ট লগ বা ট্রানজেকশনের ইউনিক রেফারেন্স লিংক হিসেবে কাজ করে।
{
  "type": "https://example.com/probs/out-of-stock",
  "title": "Out of Stock",
  "detail": "The requested product is out of stock.",
  "instance": "https://api.example.com/products/123",
  "status": 400,
  "extra": "You can add any extra information you want."
}
Enter fullscreen mode Exit fullscreen mode

আপনি চাইলে কাস্টম ফিল্ডও প্রোভাইড করতে পারেন। এখানে ফিক্সড কোনো নিয়ম নেই। যেমন ওপরে "extra" ফিল্ড ব্যবহার করা হয়েছে।


HATEOAS: API design standard

API design নিয়ে অনেক কিছুই বললাম, কিন্তু কোনো standard বা architectural style নিয়ে কোনো কথাই বলিনি। না বলার main reason হচ্ছে industry-তে already একটা popular architectural style রয়েছে। সেটা শিখে নিতে পারলেই you are good to go।

Read More from here

HATEOAS স্ট্যান্ডার্ড প্রোভাইড করলেও বর্তমানে এটি ইগনোর করা শুরু হয়ে গেছে। এর মেইন কারণ হচ্ছে এর ওভারহেড। বর্তমানে ফ্রন্টএন্ড আর্কিটেকচারে সবসময় প্রেডিক্টেবল ডাটা প্রেফার করে, যাতে খুব সহজেই ফ্রন্টএন্ডের UI অর্গানাইজ ও অপ্টিমাইজ করা যায়। কিন্তু HATEOAS-এর প্রি-বিল্ট লিঙ্ক জেনারেশন ইত্যাদির কারণে এটি রিজেকশনের দিকে চলে গেছে। আর সবকিছু মাইক্রোসার্ভিসের দিকে যাওয়ার কারণে এটি ব্যবহার করার প্রয়োজনীয়তাও আর থাকে না।


বটম লাইন

এই article-এ আমি just basic and important topic-গুলো cover করার try করেছি। এছাড়াও REST API design-এর ক্ষেত্রে আরও অনেক জিনিস আছে যেগুলো help করবে। API design-এর ওপর Stack Learner-এর একটা 9hrs free workshop আছে। আপনারা চাইলে তাদের YouTube বা official website থেকে access করতে পারেন।

আপনি কি এখনও POST /getUser টাইপের API লেখেন? সৎভাবে কমেন্টে বলুন! 😄👇

Top comments (0)