বেশিরভাগ টিউটোরিয়াল ধরে নেয় আপনার frontend ঠিক একটা পরিচ্ছন্ন, সামঞ্জস্যপূর্ণ API-র সঙ্গে কথা বলে। বাস্তব পণ্য খুব কমই তা দেয়। শেষমেশ আপনার হাতে থাকে দুটি সার্ভিস যা আলাদাভাবে বড় হয়েছে — ধরুন একটা e-commerce API আর একটা lab/diagnostics API — আলাদা response shape, আলাদা auth কনভেনশন ও file upload কীভাবে কাজ করা উচিত সে বিষয়ে আলাদা ধারণা নিয়ে। আর আপনার আছে একটা interface যাকে দুটোর উপরেই বসতে হবে যেন তারা একই জিনিস।
এখানকার ব্যর্থতার ধরনটা সূক্ষ্ম ও আমি সোজা এর মধ্যে ঢুকে পড়েছি। প্রথম backend-টা সরাসরি আপনার component-এ জুড়ে দেন। তারপর দ্বিতীয়টা আসে, তার response আলাদাভাবে গঠিত, তাই সবচেয়ে দ্রুত সমাধানের দিকে হাত বাড়ান: একটা conditional। শুধু একটা। তারপর আরেকটা। ছয় মাস পর if (service === "lab") ত্রিশটা component জুড়ে ছড়ানো, প্রতিটি একটু জ্ঞান বহন করছে যে এটা আসলে কোন backend-এর সঙ্গে কথা বলছে। UI আপনার অবকাঠামোর আকার শুষে নিয়েছে ও এখন UI না ছুঁয়ে আপনি অবকাঠামো বদলাতে পারবেন না।
যে সীমারেখা এটা ঠিক করে
সমাধানটা একটা ধারণা: component-এর জিজ্ঞেস করা উচিত তারা কী চায় ও তাদের এক স্তর নিচে ঠিক করা উচিত কোন backend উত্তর দেবে আর কীভাবে। component কখনো একটা সার্ভিসের নাম নেয় না। কখনো একটার উপর branch করে না। তারা একটা ডেটা লেয়ার কল করে যা একটা পরিচ্ছন্ন সারফেস দেখায় ও সব কুৎসিত জিনিস সেই সারফেসের পেছনে থাকে।
component-এর জিজ্ঞেস করা উচিত তারা কী চায়। এক স্তর নিচে ঠিক করে কোন backend উত্তর দেবে আর কীভাবে। UI কখনো একটা সার্ভিসের নাম নেয় না, একটার উপর branch করে না।
// Components call this. They never know which backend answers.
export const data = {
orders: makeResource("order"),
labTests: makeResource("labTest"),
};
// The resource decides routing, auth & serialization internally.
function makeResource(name: string) {
const backend = routeFor(name); // e-commerce vs lab, decided here
return {
list: (q) => backend.get(pathFor(name), q).then(deserialize(name)),
get: (id) =>
backend.get(`${pathFor(name)}/${id}`).then(deserialize(name)),
create: (body) => backend.post(pathFor(name), serialize(name, body)),
};
}যে component order-এর তালিকা render করে সেটা data.orders import করে আর .list() কল করে। বিল্ডিংয়ে যে দুটি backend আছে তার কোনো ধারণা তার নেই। ওই অজ্ঞতাই পুরো উদ্দেশ্য — এটাই দুটি সার্ভিসকে নিচে স্বাধীনভাবে চলতে দেয়।
Serialization-ই আসল জোড়
Routing সহজ অর্ধেক। যে অর্ধেক আসলে তার দাম তোলে তা হলো serialization: দুটি আলাদা response shape-কে একটা অভ্যন্তরীণ মডেলে পরিণত করা যা আপনার component বিশ্বাস করতে পারে।
যদি একটা backend created_at দেয় আর অন্যটা createdAt দেয় ও একটা customer-কে customer.data.attributes-এর নিচে nest করে যখন অন্যটা flat দেয়, আপনি চাইবেন না ওই পার্থক্য একটা component-এ পৌঁছাক। আপনি দুটোকেই সীমারেখায় একটা shape-এ normalize করেন, ভেতরে আসার পথে ও বেরোনোর পথে প্রতিটি backend-এর প্রত্যাশিত ফরম্যাটে ফিরে serialize করেন।
// Two backends, one internal model. Components only ever see Order.
const deserializers = {
order: (raw) => ({
id: raw.id,
code: raw.order_code,
createdAt: raw.created_at,
customer: raw.customer, // already flat here
}),
labTest: (raw) => ({
id: raw.id,
code: raw.test_ref,
createdAt: raw.attributes.createdAt, // nested here
customer: raw.attributes.patient,
}),
};এই জোড় একবার থাকলে আপনার component লেখা হয় Order আর LabTest-এর বিপরীতে — পরিচ্ছন্ন অভ্যন্তরীণ টাইপ — দুটি আলাদা দল যে JSON ডিজাইন করেছে তার বিপরীতে নয়। এলোমেলো জিনিস দরজায় থেমে যায়।
যেসব অংশ happy path-এ খাপ খায় না
Upload ছিল যেখানে এটা আমার কাছে তার দাম তুলেছিল। দুটি backend ফাইল আলাদাভাবে চাইত — আলাদা field নাম, আলাদা multipart কনভেনশন, একটা একটা pre-signed ধাপ আশা করত যা অন্যটার ছিল না। সেটা ছড়ালে অ্যাপের প্রতিটি form-কে জানতে হবে কোন upload নাচ করতে হবে। এটাকে ডেটা লেয়ারে ভাঁজ করা মানে একটা component কেবল upload(file) কল করে আর লেয়ার সেই resource-এর যে আচার লাগে সেটা করে।
একই কথা এমন pagination-এর জন্য যা একদিকে cursor-ভিত্তিক আর অন্যদিকে page-সংখ্যাযুক্ত, বা auth token যা আলাদা header-এ থাকে। এর প্রতিটি একটা ছোট সিদ্ধান্ত ও প্রতিটি ঠিক একটা জায়গায় থাকতে চায়। ডেটা লেয়ারই সেই জায়গা।
এটা পরে আপনাকে যা কিনে দেয়
এই দাম আগেভাগে দেওয়ার কারণ হলো এটা বদলে দেয় ভবিষ্যতের একটা migration কেমন দেখায়। যখন একটা backend প্রতিস্থাপিত, একীভূত বা versioned হয় — আর একটা লাইভ পণ্যে একদিন হবেই — পরিবর্তনটা একটা ডেটা-লেয়ার পরিবর্তন। আপনি routeFor আর deserializer আপডেট করেন, টেস্ট চালান ও UI কখনো জানতে পারে না। এর সঙ্গে বিকল্পটার তুলনা করুন: component tree জুড়ে ত্রিশটা if (service === ...) branch খুঁজে বের করা আর প্রার্থনা করা যে সব পেয়েছেন।
এটাই পুরো লেনদেন। পরে পুরো-অ্যাপ পুনর্লিখন এড়াতে আপনি এখন এক স্তর indirection মেনে নেন।
কখন এটা করা উচিত নয়
দাম নিয়ে সৎ থাকতে চাই, কারণ indirection ফ্রি নয় আর এটাকে cargo-cult করা নিজের একটা ভুল। যদি আপনার একটা backend থাকে আর দ্বিতীয়টার কোনো বাস্তব পরিকল্পনা না থাকে, এই প্যাটার্ন শুধুই overhead — এমন একটা সমস্যা থেকে রক্ষা করা সীমারেখা যা আপনার নেই। আপনার একটা API-র বিপরীতে সরাসরি লিখুন আর কাজে এগিয়ে যান।
সীমারেখা বানানোর সংকেতটা নির্দিষ্ট: এক UI-র পেছনে আপনার দুটি বা তার বেশি সত্যিই আলাদা backend আছে, অথবা আপনি একটা থেকে আরেকটায় migration-এর মাঝপথে আর দুটোই একসঙ্গে লাইভ। তখনই সার্ভিসের নামের উপর branch করা একটা component একটা ছোট সুবিধা থাকা বন্ধ করে ও সেই জিনিস হয়ে ওঠে যা চুপচাপ আপনার interface-কে আপনার অবকাঠামোর সঙ্গে ঝালাই করে দেয়। ওই ঝালাই বসার আগে সীমারেখা টানুন ও UI আপনার বদলানোর জন্য থাকে।