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

Express দিয়ে REST API — একটি অনলাইন দোকানের ব্যাকএন্ড

Building REST APIs with Express — Routing, Middleware, Validation, Errors, Security & Testing

This Bangla lesson builds the backend of an online shop with Express: REST resource design, moving from node:http to Express, the middleware pipeline, routing with Router, naming, methods, status codes and a consistent error shape, input validation, pagination, filtering and sorting, error middleware and async errors, core security rules, configuration, project structure and API testing.

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

  • REST API কী, আর একটি অনলাইন দোকানের API কীভাবে resource আর endpoint-এ সাজাতে হয়
  • Express-এর routing, req.params, req.query, req.body আর res.status().json()
  • Middleware pipeline কীভাবে চলে — আর ভেতরের কলকবজা নিজে বানিয়ে দেখা
  • যাচাই, pagination, এক ছাঁচের error, async error আর নিরাপত্তার মূল নিয়ম
  • পেশাদার প্রজেক্টের গঠন, টেস্টিং, ডকুমেন্টেশন — আর AI-এর লেখা API যাচাইয়ের চেকলিস্ট

১ · REST API — ডেটাকে ঠিকানা দেওয়া

পর্ব ৩৫-এ আপনি ব্রাউজার থেকে fetch দিয়ে সার্ভারের কাছে অনুরোধ পাঠিয়েছিলেন। এবার টেবিলের অন্য পাশে বসার পালা। ধরুন “চালডাল”-ধাঁচের একটি অনলাইন মুদি দোকান বানাচ্ছেন। ওয়েবসাইট, Android অ্যাপ আর দোকানের অ্যাডমিন প্যানেল — তিনটিই একই পণ্যের তালিকা দেখায়, একই অর্ডার নেয়। তিনটির জন্য তিনটি আলাদা ডেটার উৎস রাখা পাগলামি। দরকার একটি সার্ভার প্রোগ্রাম, যে ডেটা পাহারা দেয় আর সবাইকে একই নিয়মে ডেটা দেয়। এমন প্রোগ্রামকে বলে API.

সবচেয়ে প্রচলিত ধাঁচের নাম REST. এর মূল চিন্তা খুব সহজ: সার্ভারের প্রতিটি জিনিস একটি resource — পণ্য, অর্ডার, ব্যবহারকারী। প্রতিটি resource-এর একটি স্থায়ী ঠিকানা (URL) আছে। কী করতে চান, তা বলে HTTP method. আর সার্ভার ফল জানায় status code আর JSON body দিয়ে। প্রতিটি “method + ঠিকানা” জোড়া একটি endpoint.

Endpointকাজসফল হলে
GET /api/products?category=rice&page=2চালের তালিকা, দ্বিতীয় পাতা200 + তালিকা
GET /api/products/42একটি পণ্য200 (না থাকলে 404)
POST /api/ordersনতুন অর্ডার201 + Location
PATCH /api/orders/A-101/statusঅর্ডারের অবস্থা বদল (অ্যাডমিন)200
DELETE /api/orders/A-101অর্ডার বাতিল204, body নেই

একটি বড় পাইকারি গুদামের কথা ভাবুন। প্রতিটি তাকের একটি নম্বর আছে (URL). দোকানদার কাউন্টারে এসে একটি স্লিপ দেন: “তাক ৪২ — দেখাও” (GET), “নতুন চালান জমা নাও” (POST), “তাক ৪২ খালি করো” (DELETE). কাউন্টারের লোক (API) নিজে তাকে যান, নিয়ম মেনে কাজ করেন, আর একটি রসিদ দেন — “হয়েছে”, “নেই”, “স্লিপ ভুল লেখা”। দোকানদার কখনো সরাসরি গুদামে ঢোকেন না। এই “কখনো সরাসরি ঢুকতে না দেওয়া”-ই API-র আসল কাজ: ডেটার পাহারা।

মূল ধারণা

REST API হলো একটি সার্ভার প্রোগ্রাম, যে আপনার ডেটাকে URL-এ resource হিসেবে সাজিয়ে দেয় আর client-কে HTTP method দিয়ে সেগুলো পড়তে ও বদলাতে দেয়; উত্তর আসে status code আর JSON-এ। প্রতিটি অনুরোধ নিজেই সম্পূর্ণ — সার্ভার আগের অনুরোধ মনে রেখে কাজ করে না (stateless), তাই একই API একসাথে হাজারো client সামলাতে পারে।

২ · node:http থেকে Express — framework কেন লাগে

পর্ব ৪৯-এ দেখেছেন, Node.js-এর built-in node:http module দিয়েই একটি server চালানো যায়। প্রতিটি অনুরোধে Node আপনার function-কে দুটি object দেয়: req (অনুরোধ — method, url, header, আর body-র stream) আর res (উত্তর লেখার খাতা)। res.writeHead(status, headers) দিয়ে status আর header, res.end(text) দিয়ে body পাঠিয়ে শেষ:

const http = require("node:http");
http.createServer((req, res) => {
  if (req.method === "GET" && req.url === "/api/products") {
    res.writeHead(200, { "Content-Type": "application/json" });
    return res.end(JSON.stringify([{ id: 1, name: "চাল" }]));
  }
  // GET /api/products/42 ? ঠিকানা কেটে id বের করো, ?category=rice আলাদা করো,
  // POST হলে body-র টুকরো জোড়া দাও, JSON.parse-এ ভুল হলে 400, বড় body ঠেকাও,
  // প্রতিটি অনুরোধ log করো, লগইন যাচাই করো, কোথাও throw হলে 500 ... সব হাতে!
  res.writeHead(404).end();
}).listen(3000);

মন্তব্যগুলোই সমস্যার তালিকা। দশটি endpoint হলে দশটি if, প্রতিটিতে একই body-পড়া, একই error-সামলানো, একই log-লেখা। এই পুনরাবৃত্তি থেকে মুক্তি দিতেই Express. এটি ছোট, জনপ্রিয় একটি framework — বিশাল কিছু নয়, মূলত দুটি জিনিস যোগ করে:

  • Routing: “এই method আর এই ঠিকানার জন্য এই function” — এমন নিয়ম ঘোষণা করা, ঠিকানার ভেতরের অংশ (:id) নিজে থেকে বের করা।
  • Middleware pipeline: প্রতিটি request কয়েকটি function-এর শিকল পেরিয়ে যায়; প্রতিটি function অনুরোধ পড়তে পারে, বদলাতে পারে, উত্তর দিয়ে দিতে পারে, অথবা পরের জনের হাতে দিতে পারে।

Express লেখেন TJ Holowaychuk; ২০০৯ সালে কাজ শুরু, ২০১০ সালে প্রথম স্থিতিশীল সংস্করণ। অনুপ্রেরণা ছিল Ruby-র ছোট্ট web framework Sinatra — যেখানে get '/hello' do ... end লিখলেই একটি route. Express দ্রুত Node-এর প্রায় “ডিফল্ট” framework হয়ে ওঠে; MEAN/MERN stack-এর “E” এটিই। Express 4 আসে ২০১৪ সালে, তারপর দীর্ঘ নীরবতা — Express 5 চূড়ান্তভাবে প্রকাশিত হয় ২০২৪ সালে (ভাবনার প্রশ্ন ১)। এই পর্বের কোড Express 5 ধরে লেখা; যেখানে Express 4-এ ভিন্ন, তা আলাদা করে বলা হয়েছে।

প্রথম Express অ্যাপ

Express একটি npm package (পর্ব ৪৬), তাই আগে ইনস্টল। পাতার ভেতরের রানারে npm package চলে না, তাই নিচের Express কোডগুলো নিজের কম্পিউটারে চালাবেন — প্রত্যাশিত আউটপুট সাথে দেওয়া আছে।

mkdir shop-api && cd shop-api
npm init -y
npm pkg set type=module        # import/export (পর্ব ৪৫)
npm i express
import express from "express";

const app = express();
app.use(express.json());                       // JSON body পড়ে req.body-তে রাখে

const products = [
  { id: 42, name: "মিনিকেট চাল ৫ কেজি", category: "rice", price: 420 },
  { id: 43, name: "মসুর ডাল ১ কেজি", category: "dal", price: 160 },
];

app.get("/api/products", (req, res) => {
  const { category } = req.query;              // ?category=rice → "rice"
  res.json(category ? products.filter((p) => p.category === category) : products);
});

app.get("/api/products/:id", (req, res) => {
  const product = products.find((p) => p.id === Number(req.params.id));  // params সবসময় string
  if (!product) return res.status(404).json({ error: { code: "NOT_FOUND", message: "পণ্যটি পাওয়া যায়নি" } });
  res.json(product);                           // status না দিলে 200
});

app.delete("/api/products/:id", (req, res) => {
  // ... মুছে ফেলার কাজ ...
  res.sendStatus(204);                         // সফল, কিন্তু body নেই
});

app.listen(process.env.PORT ?? 3000, () => console.log("API চলছে"));
$ curl -i "http://localhost:3000/api/products?category=rice"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

[{"id":42,"name":"মিনিকেট চাল ৫ কেজি","category":"rice","price":420}]

$ curl -i http://localhost:3000/api/products/99
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8

{"error":{"code":"NOT_FOUND","message":"পণ্যটি পাওয়া যায়নি"}}

প্রত্যাশিত আউটপুট উপরে (header-এর কিছু লাইন বাদ দেওয়া)। চারটি জিনিস মনে রাখুন। req.params — ঠিকানার :id অংশ, সবসময় string; তাই Number(...). req.query — ?-এর পরের অংশ, এটিও string (আর একই নাম দুবার এলে array!)। req.body — express.json() middleware JSON body পড়ে এখানে রাখে; এটি না দিলে, বা client Content-Type: application/json না পাঠালে, এখানে আপনার ডেটা থাকে না (Express 5-এ undefined). আর res.status(code).json(data) একসাথে status, Content-Type আর body লিখে দেয়; res.sendStatus(204) শুধু status.

৩ · Middleware — প্রতিটি request-এর পাইপলাইন

Express-এর হৃদয় middleware. একটি middleware হলো তিন প্যারামিটারের একটি function — (req, res, next). এর সামনে চারটি পথ খোলা: req পড়া, req-এ কিছু যোগ করা (যেমন req.user), নিজেই উত্তর দিয়ে শিকল থামিয়ে দেওয়া, অথবা next() ডেকে পরের জনের হাতে তুলে দেওয়া। আর next(err) ডাকলে মাঝের সব ধাপ বাদ দিয়ে সোজা error handler-এ।

বিমানবন্দরের কথা ভাবুন। গেটে টিকিট দেখা, তারপর নিরাপত্তা তল্লাশি, তারপর ইমিগ্রেশন, তারপর বোর্ডিং। প্রত্যেকে নিজের কাজ করে “এগিয়ে যান” বলেন (next())। কারও কাছে সমস্যা পেলে সেখানেই থামিয়ে দেন — আপনি আর বোর্ডিং-এ পৌঁছান না। ক্রমটিও গুরুত্বপূর্ণ: তল্লাশির আগে বোর্ডিং হলে পুরো ব্যবস্থাই অর্থহীন।

নিচের ডায়াগ্রামে একটি POST /api/orders অনুরোধ শিকল ধরে এগোচ্ছে। প্রতিটি ধাপে req-এর অবস্থা খেয়াল করুন — আর শেষ ধাপে দেখুন একটি error কীভাবে লাফিয়ে চলে যায়:

  1. RequestPOST /api/ordersসবে এসেছে
  2. loggernext()সময় শুরু
  3. express.json()req.bodybody পড়া
  4. requireAuthreq.userপরিচয় যাচাই
  5. Route handlerres.status(201)আসল কাজ
  6. Error handler(err, req, res, next)সব ভুলের শেষ ঠিকানা
reqNode থেকে সদ্য পাওয়া
method:  "POST"
url:     "/api/orders"
headers: { "content-type": "application/json",
           authorization: "Bearer demo-token-123" }
body:    (এখনো পড়া হয়নি — stream-এ ভাসছে)
loggerকিছু বদলায় না, শুধু নোট রাখে
const start = performance.now();
res.on("finish", () => console.log(/* method, url, status, সময় */));
next();   // এগিয়ে যান
req.bodyJSON পড়ে object বানানো হলো
body: { phone: "01712345678",
        items: [{ productId: 42, qty: 2 }] }
// body ১০০ KB-র বেশি হলে → next(413 error)
// ভাঙা JSON হলে          → next(400 error)
req.usertoken মিলেছে
user: { id: "u1", role: "customer" }
// token না থাকলে বা ভুল হলে এখানেই
// res.status(401).json(...) — handler পর্যন্ত যাওয়াই হয় না
Handlerযাচাই, অর্ডার তৈরি, উত্তর
res.status(201)
   .location("/api/orders/A-101")
   .json({ id: "A-101", status: "pending", ... });
// এখানে next() ডাকা হয় না — উত্তর দেওয়া মানেই শিকল শেষ
Terminallogger-এর finish event
POST /api/orders 201 12.4ms
// উত্তর পাঠানো শেষ হলে logger-এর রেখে যাওয়া listener চলল
Error-এর পথhandler-এ ডেটাবেস ভেঙে পড়ল
handler:  throw new Error("connection refused")
          ↓ Express 5 নিজে ধরে next(err) ডাকে
          ↓ মাঝের সাধারণ middleware সব বাদ
error handler → 500
{ "error": { "code": "INTERNAL",
             "message": "সার্ভারে একটি সমস্যা হয়েছে" } }
// আসল বার্তা আর stack trace শুধু সার্ভারের log-এ
  1. অনুরোধ এলো। Node একটি req আর একটি খালি res বানিয়ে Express-কে দিল। Body এখনো পড়া হয়নি — সেটি টুকরো টুকরো আসা একটি stream (পর্ব ৪৯)। Express এখন app.use আর route-গুলো যে ক্রমে লেখা হয়েছে, সেই ক্রমে একে একে চালাবে।
  2. logger. প্রথম middleware শুধু শুরুর সময় নোট করে, আর res-এ একটি “উত্তর শেষ হলে আমাকে ডেকো” listener রেখে next() ডাকে। এটি next() ডাকতে ভুলে গেলে? অনুরোধ চিরকাল ঝুলে থাকবে — client শুধু ঘুরতে থাকা চাকা দেখবে। Express-এর সবচেয়ে চেনা বাগগুলোর একটি এটি।
  3. express.json(). Built-in middleware stream পড়ে, Content-Type JSON কি না দেখে, JSON.parse করে req.body-তে রাখে। আকার সীমার (ডিফল্ট ১০০ KB) বেশি হলে বা JSON ভাঙা হলে নিজেই next(err) ডাকে — 413 বা 400.
  4. requireAuth. আপনার নিজের লেখা middleware: Authorization header থেকে token নিয়ে যাচাই করে, মিললে req.user বসিয়ে next(). না মিললে নিজেই 401 পাঠিয়ে শিকল থামায়। পরের সবাই নিশ্চিন্তে ধরে নিতে পারে, req.user আছে।
  5. Route handler. এতক্ষণে আসল কাজ — যাচাই, অর্ডার তৈরি, 201 উত্তর। Handler-ও আসলে একটি middleware, শুধু সাধারণত শিকলের শেষটি। একটি অনুরোধে উত্তর পাঠানো যায় মাত্র একবার; দুবার পাঠাতে গেলে Node-এর ERR_HTTP_HEADERS_SENT error আসে।
  6. উত্তর চলে গেল। উত্তর পাঠানো শেষ হতেই logger-এর রেখে যাওয়া listener চলল আর এক লাইনে method, ঠিকানা, status আর সময় লিখল। এভাবে প্রথম middleware শেষেও কাজ করতে পারে — timing, metrics, audit log সব এই কৌশলে।
  7. Error-এর পথ। ধরুন handler-এ ডেটাবেস সংযোগ ভেঙে exception হলো। Express 5 handler-এর ফেরত দেওয়া Promise reject হলেও নিজে ধরে next(err) ডাকে; তখন মাঝের সব সাধারণ middleware বাদ দিয়ে Express খোঁজে চার প্যারামিটারের error-handling middleware. সেটি ব্যবহারকারীকে নিরাপদ, সাধারণ একটি বার্তা দেয়; আসল বিবরণ যায় শুধু সার্ভারের log-এ।

তিন রকম middleware

🧩 Built-in

Express-এর সাথেই আসে: express.json(), express.urlencoded() (HTML ফর্মের জন্য), express.static("public") (ছবি, CSS-এর মতো স্থির ফাইল)।

npm i লাগে না

📦 Third-party

helmet (নিরাপত্তা header), cors (অন্য origin-এর অনুমতি), morgan (request log), express-rate-limit (অনুরোধের সীমা)।

npm থেকে

✍️ নিজের লেখা

logger, সময় মাপা, লগইন যাচাই, ভূমিকা যাচাই (requireRole("admin")), request id বসানো।

সাধারণ function
// শুধু শেখার জন্য নকল token — আসল অ্যাপে session বা JWT (অংশ ৭)
const TOKENS = { "demo-token-123": { id: "u1", role: "customer" },
                 "admin-token-9":  { id: "a1", role: "admin" } };

export function requireAuth(req, res, next) {
  const [scheme, token] = (req.get("authorization") ?? "").split(" ");
  const user = scheme === "Bearer" ? TOKENS[token] : undefined;
  if (!user) return res.status(401).json({ error: { code: "UNAUTHENTICATED", message: "আগে লগইন করুন" } });
  req.user = user;                              // পরের সবার জন্য
  next();
}

export const requireRole = (role) => (req, res, next) =>   // middleware বানানোর function
  req.user?.role === role ? next()
    : res.status(403).json({ error: { code: "FORBIDDEN", message: "এই কাজের অনুমতি আপনার নেই" } });

requireRole নিজে middleware নয় — middleware ফেরত দেয়। এটি পর্ব ২২-এর closure আর পর্ব ১৬-এর higher-order function-এর সরাসরি ব্যবহার: role মনে রেখে একটি নতুন (req, res, next) function বানানো। 401 আর 403-এর পার্থক্যও খেয়াল করুন: ৪০১ মানে “আপনি কে জানি না”, ৪০৩ মানে “চিনি, কিন্তু অনুমতি নেই”।

ক্রমই নিয়ম। app.use(express.json()) যদি route-গুলোর পরে লেখেন, route চলার সময় req.body খালি থাকবে। requireAuth যদি express.static-এর আগে বসান, ছবি দেখতেও লগইন লাগবে। Express উপর থেকে নিচে পড়ে, প্রথম যে উত্তর দেয় সেখানেই থামে।

৪ · Routing আর Router — আর Express-এর ভেতরে উঁকি

Express কীভাবে বোঝে GET /api/products/42 কোন handler-এর? প্রতিটি route-এর ঠিকানা-ছাঁচ (/api/products/:id) ভেতরে একটি regular expression-এ বদলে যায়। অনুরোধ এলে Express route-গুলো ঘোষণার ক্রমে একে একে মিলিয়ে দেখে — method আর path দুটোই মিললে থামে, আর :id-এর জায়গায় যা ছিল তা রাখে req.params-এ:

1app.get("/api/products", list);2app.post("/api/products", create);3app.get("/api/products/:id", getOne);4app.delete("/api/products/:id", remove);
Express-এর হিসাব
path
req.query
তুলনা
req.params
  1. ঠিকানা ভাগ করা। URL-এর ?-এর আগের অংশ path, পরের অংশ query. Query routing-এ কোনো ভূমিকা রাখে না — শুধু parse হয়ে req.query-তে যায়। তাই /api/products/42?sort=price আর /api/products/42 একই route-এ পৌঁছায়।
  2. Route ১ — মিলল না। Method তো GET-ই, কিন্তু ছাঁচে অংশ দুটি (api, products), অনুরোধে তিনটি। পুরো path মিলতে হয়, শুধু শুরুটুকু নয়। (app.use-এর বেলায় নিয়ম আলাদা — সেখানে শুরুটুকু মিললেই চলে, তাই app.use("/api", ...) সব /api/... ধরে।)
  3. Route ২ — method আলাদা। Path-এর অংশও তো মেলে না, কিন্তু তার আগেই method POST বনাম GET. একই ঠিকানায় ভিন্ন method মানে ভিন্ন handler — এটিই REST-এর ভিত্তি।
  4. Route ৩ — মিলেছে। :id একটি “ফাঁকা ঘর”, যেকোনো একটি অংশ ধরে। Express ধরে রাখা লেখাটি দেয় req.params.id-তে — খেয়াল করুন, '42' একটি string, সংখ্যা নয়। ডেটাবেসে খোঁজার আগে রূপান্তর আর যাচাই আপনার দায়িত্ব: /api/products/abc এলেও এই route-ই মিলবে।
  5. Handler চলল, তালিকা থামল। প্রথম মিলেই খোঁজা শেষ — route ৪ আর দেখাই হয়নি। এ কারণেই ক্রম গুরুত্বপূর্ণ: /api/products/:id-এর পরে /api/products/featured লিখলে “featured” কখনো পৌঁছাবে না, কারণ :id সেটিকে { id: 'featured' } হিসেবে আগেই ধরে ফেলবে। নির্দিষ্ট ঠিকানা আগে, ফাঁকা-ঘরওয়ালা পরে।

express.Router — প্রতিটি resource-এর জন্য আলাদা ফাইল

সব route এক ফাইলে রাখলে কয়েক মাসেই হাজার লাইন। express.Router() একটি “ছোট অ্যাপ” বানায় — নিজস্ব route আর middleware-সহ — যাকে মূল অ্যাপের একটি ঠিকানায় বসানো (mount) যায়:

import { Router } from "express";
export const productsRouter = Router();

productsRouter.get("/", listProducts);          // GET  /api/products
productsRouter.get("/:id", getProduct);         // GET  /api/products/:id
productsRouter.post("/", requireAuth, requireRole("admin"), createProduct);  // শুধু এই route-এ
app.use("/api/products", productsRouter);       // "/api/products" + "/:id"
app.use("/api/orders", requireAuth, ordersRouter);  // orders-এর সব route-এ লগইন লাগবে

Router-এর ভেতরে ঠিকানা লেখা হয় mount-এর তুলনায় — "/" মানে /api/products. খেয়াল করুন, middleware তিন স্তরে বসানো যায়: পুরো অ্যাপে (app.use(fn)), একটি router-এ (app.use("/api/orders", requireAuth, ...)), বা একটি route-এ (post("/", requireAuth, handler)).

নিজে বানিয়ে দেখুন — Express-এর ভেতরে কী আছে

Express-এর জাদু আসলে কয়েক ডজন লাইনের ধারণা। নিচের প্রোগ্রামটি Node 20-এ চলে, কোনো package ছাড়া — শুধু node:http. এতে আছে use() আর route(), :id-কে regex-এর named group-এ বদলে মেলানো, আকার-সীমাসহ JSON parser, logger, 404 আর একটি error handler. তারপর একই প্রোগ্রাম fetch দিয়ে নিজের কাছেই ছয়টি অনুরোধ পাঠায় (port 0 মানে “যেকোনো খালি port”):

const http = require("node:http");
const chain = [], routes = [], use = (fn) => chain.push(fn);
const route = (method, path, fn) => routes.push({ method, fn, re: RegExp(`^${path.replace(/:(\w+)/g, "(?<$1>[^/]+)")}$`) });
const db = new Map([[1, { id: 1, name: "চাল", price: 420 }], [2, { id: 2, name: "ডাল", price: 160 }]]);
const notFound = { status: 404, message: "পণ্যটি নেই" };
use((req, res, next) => { console.log("→", req.method, req.url); next(); });                 // ১. logger
use(async (req, res, next) => {                                                               // ২. JSON parser
  let raw = ""; for await (const c of req) if ((raw += c).length > 1000) return next({ status: 413, message: "body খুব বড়" });
  try { req.body = raw ? JSON.parse(raw) : {}; next(); } catch { next({ status: 400, message: "ভাঙা JSON" }); }
});
use((req, res, next) => {                                                                     // ৩. router
  const r = routes.find((r) => r.method === req.method && r.re.test(req.url));
  if (!r) return next({ status: 404, message: "এই ঠিকানা নেই" });
  req.params = { ...req.url.match(r.re).groups }; return r.fn(req, res, next);
});
route("GET", "/api/products", (req, res) => res.json(200, [...db.values()]));
route("GET", "/api/products/:id", (req, res, next) => db.has(+req.params.id) ? res.json(200, db.get(+req.params.id)) : next(notFound));
route("DELETE", "/api/products/:id", (req, res, next) => db.delete(+req.params.id) ? res.writeHead(204).end() : next(notFound));
route("POST", "/api/products", (req, res, next) => {
  const { name, price } = req.body;
  if (!name || !(price > 0)) return next({ status: 422, message: "name আর ধনাত্মক price দরকার" });
  const p = { id: Math.max(0, ...db.keys()) + 1, name, price }; db.set(p.id, p);
  res.setHeader("Location", `/api/products/${p.id}`); res.json(201, p);
});
const server = http.createServer((req, res) => {
  res.json = (s, d) => res.writeHead(s, { "Content-Type": "application/json" }).end(JSON.stringify(d));
  let i = 0;
  const next = (err) => err ? res.json(err.status ?? 500, { error: err.status ? err.message : "সার্ভারে সমস্যা" }) // ৪. error handler
    : Promise.resolve().then(() => chain[i++](req, res, next)).catch(next);
  next();
}).listen(0, "127.0.0.1", async () => {
  for (const [method, path, body] of [["GET", "/api/products/2"], ["POST", "/api/products", { name: "তেল", price: 190 }],
    ["POST", "/api/products", { name: "" }], ["DELETE", "/api/products/1"], ["GET", "/api/products"], ["GET", "/api/orders"]]) {
    const res = await fetch(`http://127.0.0.1:${server.address().port}${path}`, { method, body: body && JSON.stringify(body) });
    console.log(`   ${res.status} ${await res.text()} ${res.headers.get("location") ?? ""}`.trimEnd());
  }
  server.close();
});

আউটপুটে প্রতিটি অনুরোধের জন্য দুটি লাইন: সার্ভারের logger-এর → GET /api/products/2, তারপর client-এর দেখা উত্তর। ক্রমে পাবেন 200 (একটি পণ্য), 201 {"id":3,"name":"তেল","price":190} /api/products/3 (শেষে Location header), 422 {"error":"name আর ধনাত্মক price দরকার"}, body ছাড়া 204, বাকি দুটি পণ্যের তালিকা, আর 404 {"error":"এই ঠিকানা নেই"}. পুরো রহস্য next-এ: এটি শিকলের পরের function ডাকে, আর error পেলে সোজা উত্তর লিখে দেয়। Promise.resolve().then(...).catch(next) লাইনটিই Express 5-এর async-error সুবিধার ক্ষুদ্র রূপ — handler-এ throw হোক বা async function reject করুক, ধরা পড়ে error handler-এ যায়। আর error handler শুধু ইচ্ছা করে দেওয়া status-ওয়ালা error-এর বার্তা দেখায়; অপ্রত্যাশিত error-এ শুধু “সার্ভারে সমস্যা” — ভেতরের খবর ফাঁস হয় না। আসল Express-এ এর ওপর আছে query parse, res.status().json()-এর মতো সুবিধা, router-এর ভেতরে router, আর বহু বছরের ধার-ঘষা।

৫ · REST নকশা — নাম, method, status আর error-এর এক ছাঁচ

Express আপনাকে যেকোনো ঠিকানা আর যেকোনো status ব্যবহার করতে দেয় — GET /deleteOrder?id=5 লিখলেও আটকাবে না। ভালো API-র শৃঙ্খলা আসে নিয়ম থেকে, framework থেকে নয়। ভালো API “অনুমানযোগ্য” — একটি endpoint দেখলে বাকিগুলো আন্দাজ করা যায়।

নামকরণ — বহুবচন নামপদ, ক্রিয়া নয়

  • Resource-এর নাম বহুবচন নামপদ: /api/products, /api/orders — /api/getProducts বা /api/product/create নয়। কাজ বলে method.
  • একটির ঠিকানা: /api/products/42. সম্পর্ক: /api/users/u1/orders (u1-এর অর্ডারগুলো)। দুই স্তরের বেশি গভীর না করাই ভালো।
  • যে কাজ সহজ CRUD-এ পড়ে না (অর্ডারের অবস্থা বদল), সেটিকেও resource-এর অংশ ভাবুন: PATCH /api/orders/A-101/status, body-তে { "status": "shipped" }.
  • ঠিকানায় ছোট হাতের অক্ষর আর হাইফেন (/api/delivery-zones); JSON-এর key-তে একটি রীতি বেছে সবখানে মানুন (JavaScript-জগতে সাধারণত camelCase).

Method আর idempotency

পর্ব ৩৫-এর idempotent ধারণা এখন আপনার দায়িত্ব। GET কখনো কিছু বদলাবে না (crawler আর prefetch নিশ্চিন্তে GET পাঠায় — GET /api/orders/5/cancel থাকলে কোনো একদিন বট আপনার অর্ডার বাতিল করে দেবে)। PUT পুরো resource বদলে বসায়, DELETE মুছে — দুটোই দশবার পাঠালেও ফল এক, তাই client নিশ্চিন্তে আবার চেষ্টা করতে পারে। POST idempotent নয় — দুবার এলে দুটি অর্ডার; পেমেন্টের মতো জায়গায় Idempotency-Key header নিয়ে সার্ভারে আগের key মনে রাখা হয়। PATCH শুধু দেওয়া অংশ বদলায়।

প্রতিটি কাজের সঠিক status code

পরিস্থিতিStatusExpress-এ
পড়া বা বদলানো সফল200 OKres.json(data)
নতুন resource তৈরি201 Created + Locationres.status(201).location(url).json(obj)
সফল, ফেরত দেওয়ার কিছু নেই (DELETE)204 No Contentres.sendStatus(204)
অনুরোধের গঠনই ভুল (ভাঙা JSON)400 Bad Requestexpress.json() নিজেই দেয়
লগইন নেই বা token ভুল401 Unauthorizedauth middleware
লগইন আছে, অনুমতি নেই403 Forbiddenrole middleware
এমন কিছু নেই404 Not Foundhandler বা শেষের catch-all
বর্তমান অবস্থার সাথে সংঘাত (ইমেইল আগেই আছে, ডেলিভারি হয়ে যাওয়া অর্ডার বাতিল)409 Conflictservice-এর error
গঠন ঠিক, মান অগ্রহণযোগ্য422 Unprocessable Contentvalidation
অনুরোধ খুব ঘন ঘন429 Too Many Requestsrate limiter
সার্ভারের নিজের ভুল500 Internal Server Errorerror middleware

সবচেয়ে বড় পাপ: সবকিছুতে 200, আর body-তে { "success": false }. তখন client-এর res.ok (পর্ব ৩৫), ব্রাউজারের cache, monitoring-এর error গণনা, retry-র যুক্তি — সব মিথ্যা তথ্য পায়। Status code হলো HTTP-র সাধারণ ভাষা; যে প্রোগ্রাম আপনার কোড পড়েনি, সেও এটি বোঝে।

Error-এর এক ছাঁচ

এক endpoint { "msg": "..." }, আরেকটি { "error": "..." }, তৃতীয়টি সাদা লেখা ফেরত দিলে front-end-কে প্রতিটির জন্য আলাদা কোড লিখতে হয়। একটি error envelope ঠিক করে সবখানে মানুন:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "কিছু তথ্য ঠিক নেই",
    "fields": { "phone": "১১ অঙ্কের সঠিক মোবাইল নম্বর দিন", "items.0.qty": "পরিমাণ ১ থেকে ২০" }
  }
}

code প্রোগ্রামের জন্য — স্থির, ইংরেজি, কখনো বদলায় না (front-end এটি দেখে সিদ্ধান্ত নেয়); message মানুষের জন্য — বাংলায়, বদলাতে পারে; fields ফর্মের কোন ঘরে কী সমস্যা। চাইলে একটি আন্তর্জাতিক মানও আছে: RFC 9457-এর “Problem Details” (application/problem+json). কোনটি বাছলেন তার চেয়ে গুরুত্বপূর্ণ, সব endpoint একই ছাঁচ মানছে কি না।

৬ · ইনপুট যাচাই, pagination, filter আর sort

পর্ব ৩৯-এ ফর্মের যাচাই শিখেছেন আর দেখেছেন কেন ব্রাউজারের যাচাই শুধু সুবিধার জন্য; পর্ব ৪৩-এর নীতিও একই — প্রতিটি সিদ্ধান্ত সার্ভারে আবার যাচাই। এখন সেই নিয়মের অন্য প্রান্ত: সার্ভারে আসা প্রতিটি byte শত্রুর পাঠানো ধরে নিন। আপনার সুন্দর ফর্ম বাদ দিয়েও যে কেউ curl, Postman বা DevTools থেকে সরাসরি অনুরোধ পাঠাতে পারে — qty: -5, price: 1, role: "admin", দশ লাখ item. তাই server-side validation ঐচ্ছিক নয়।

ধাপ ১ · অনুরোধফর্ম এড়িয়ে পাঠানো
POST /api/orders
Authorization: Bearer demo-token-123
Content-Type: application/json

{ "phone": "0171234",
  "items": [{ "productId": 42, "qty": 0, "price": 1 }] }
ধাপ ২ · validateOrderসব ভুল একসাথে জমা
phone  "0171234"  /^01[3-9]\d{8}$/  ✗ ১১ অঙ্ক নয়
items  array, ১টি               ✓
items.0.qty  0   ১–২০?          ✗
items.0.price    — পড়াই হয় না, দাম আসে ডেটাবেস থেকে
ধাপ ৩ · উত্তর422 — field ধরে ধরে
HTTP/1.1 422 Unprocessable Content

{ "error": { "code": "VALIDATION_FAILED",
    "message": "কিছু তথ্য ঠিক নেই",
    "fields": { "phone": "১১ অঙ্কের সঠিক মোবাইল নম্বর দিন",
                "items.0.qty": "পরিমাণ ১ থেকে ২০" } } }
ধাপ ৪ · ঠিক করে আবারফর্ম লাল ঘর দেখিয়েছিল
{ "phone": "01712345678",
  "items": [{ "productId": 42, "qty": 2 }] }

validateOrder → null (কোনো ভুল নেই)
orderService.create(req.user.id, body)
  → দাম: ডেটাবেস থেকে ৪২০ × ২ = ৮৪০
ধাপ ৫ · উত্তর201 + Location
HTTP/1.1 201 Created
Location: /api/orders/A-101
Content-Type: application/json; charset=utf-8

{ "id": "A-101", "status": "pending", "total": 840,
  "items": [{ "productId": 42, "qty": 2, "unitPrice": 420 }] }
ধাপ ৬ · clientLocation ধরে নতুন অর্ডার
GET /api/orders/A-101       → 200
GET /api/orders/A-102       → 404  (অন্যের অর্ডার — ৪০৩ নয়, কেন? অংশ ৭)
  1. দুষ্টু অনুরোধ। কেউ ফর্ম এড়িয়ে সরাসরি পাঠাল: ছোট ফোন নম্বর, পরিমাণ শূন্য, আর একটি বাড়তি price: 1 — যদি সার্ভার এটি বিশ্বাস করে, ৪২০ টাকার চাল ১ টাকায়! একে বলে parameter tampering.
  2. যাচাই — সব ভুল একসাথে। প্রথম ভুলেই থেমে না গিয়ে সব field দেখে সব সমস্যা জমা করা হয়, যাতে ব্যবহারকারী একবারেই সব ঠিক করতে পারেন। আর খেয়াল করুন: অজানা field (price) পড়াই হয় না। শুধু যে field আপনি চেনেন, সেগুলোই হাতে তুলে নিন — পুরো req.body সোজা ডেটাবেসে দিলে mass assignment-এর দরজা খোলে।
  3. 422 আর field-এর তালিকা। JSON তো ঠিকঠাক parse হয়েছে, তাই 400 নয়; মানগুলো অগ্রহণযোগ্য, তাই 422. Error envelope-এর fields দেখে front-end ঠিক ঘরের নিচে লাল লেখা দেখায় (পর্ব ৩৯)। কিছুই তৈরি হয়নি, তাই Location নেই।
  4. ঠিক অনুরোধ। এবার যাচাই null ফেরত দিল। Handler ব্যবসার কাজ service-কে দেয়: দাম আসে সার্ভারের নিজের ডেটা থেকে, অর্ডারের মালিক আসে req.user থেকে — body-তে userId পাঠালেও সেটি উপেক্ষিত।
  5. 201 Created. নতুন অর্ডারের স্থায়ী ঠিকানা Location header-এ, আর body-তে পুরো অর্ডার — সার্ভারের দেওয়া id, অবস্থা আর মোট দামসহ, যাতে client-কে আবার জিজ্ঞেস করতে না হয়।
  6. Location ধরে পড়া। Client চাইলে Location-এর ঠিকানায় GET করে অর্ডার দেখতে পারে। কিন্তু একই ব্যবহারকারী অন্যের অর্ডার A-102 চাইলে? সার্ভারকে অবশ্যই যাচাই করতে হবে অর্ডারটি req.user-এর কি না — API নিরাপত্তার এক নম্বর ঝুঁকি এখানেই (অংশ ৭, ভাবনার প্রশ্ন ৫)।

Express-এ ব্যবহার এমন দেখায় — যাচাইয়ের function আলাদা রাখলে সেটি টেস্ট করাও সহজ:

ordersRouter.post("/", async (req, res) => {
  const invalid = validateOrder(req.body);
  if (invalid) return res.status(422).json({ error: invalid.error });
  const { phone, items } = req.body;            // শুধু চেনা field
  const order = await orderService.create(req.user.id, { phone, items });
  res.status(201).location(`/api/orders/${order.id}`).json(order);
});

ordersRouter.patch("/:id/status", requireRole("admin"), async (req, res) => {
  const allowed = ["pending", "packed", "shipped", "delivered", "cancelled"];
  if (!allowed.includes(req.body?.status))
    return res.status(422).json({ error: { code: "VALIDATION_FAILED", message: "অবস্থাটি অজানা",
                                           fields: { status: `এর একটি দিন: ${allowed.join(", ")}` } } });
  res.json(await orderService.setStatus(req.params.id, req.body.status));  // না পেলে service 404 ছোড়ে
});

হাতে লেখা যাচাই ছোট API-তে চলে, কিন্তু field বাড়লে ঝামেলা বাড়ে। তখন একটি schema library ব্যবহার করুন — JavaScript-জগতে সবচেয়ে জনপ্রিয় Zod: z.object({ phone: z.string().regex(...), items: z.array(...).min(1) }) লিখে schema.safeParse(req.body) ডাকলে যাচাই, অজানা field বাদ দেওয়া আর TypeScript-এর type (পর্ব ৪৭) — তিনটিই এক জায়গা থেকে আসে। Fastify-র মতো framework JSON Schema দিয়ে একই কাজ নিজেই করে।

Pagination, filter আর sort

দোকানে দশ হাজার পণ্য হলে GET /api/products একবারে সব পাঠালে মোবাইল ডেটা আর সার্ভার — দুটোই কাঁদবে। তাই তালিকা আসে পাতায় পাতায় (pagination), আর query দিয়ে ছাঁকা ও সাজানো হয়: /api/products?category=rice&page=2&limit=5&sort=-price (“-” মানে বেশি থেকে কম)। প্রতিটি query মান string আর পুরোপুরি client-এর হাতে, তাই এখানেও যাচাই — আর সীমা:

1function paginate(list, q) {2  const page = Math.max(1, parseInt(q.page) || 1);3  const limit = Math.min(50, Math.max(1, parseInt(q.limit) || 10));4  const offset = (page - 1) * limit;5  const data = list.slice(offset, offset + limit);6  return { data, page, limit, total: list.length,7           totalPages: Math.ceil(list.length / limit) };8}
মানগুলো (মোট ২৩টি পণ্য)
q
page
limit
offset
slice
totalPages
  1. Query এসেছে string হয়ে। ?page=2&limit=5 থেকে req.query = { page: '2', limit: '5' }. সংখ্যা নয়, লেখা। কেউ ?page=abc, ?page=-3 বা ?page=1&page=2 (তখন array!) পাঠাতেই পারে।
  2. page. parseInt('2') = 2. সংখ্যা না হলে NaN, আর NaN || 1 = 1; Math.max(1, …) ঋণাত্মক বা শূন্য ঠেকায়। ফল: সবসময় ১ বা তার বেশি একটি পূর্ণসংখ্যা।
  3. limit — উপরের সীমাসহ। ডিফল্ট ১০, নিচে ১, আর উপরে ৫০. এই সীমা না থাকলে কেউ ?limit=1000000 দিয়ে এক অনুরোধে পুরো ডেটাবেস টেনে সার্ভার বসিয়ে দিতে পারে — একটি সহজ resource-exhaustion আক্রমণ।
  4. offset আর slice. আগের পাতাগুলোতে (page − 1) × limit = ৫টি গেছে, তাই এই পাতা শুরু ৬ষ্ঠ থেকে: slice(5, 10). ডেটাবেসে একই হিসাব হয় LIMIT 5 OFFSET 5 দিয়ে (পর্ব ৫২)।
  5. মেটাডেটা। শুধু ডেটা নয়, সাথে page, limit, total আর totalPages = ⌈২৩ ÷ ৫⌉ = ৫ — যাতে front-end “পাতা ২ / ৫” আর “পরের পাতা” বোতাম বানাতে পারে।
  6. দুষ্টু query. ?page=abc&limit=1000: page পড়ে গেল ডিফল্ট ১-এ, limit আটকে গেল ৫০-এ। কোনো crash নেই, কোনো বিশাল উত্তর নেই। (চাইলে এমন ক্ষেত্রে চুপচাপ ঠিক না করে 400/422 দেওয়াও একটি বৈধ নীতি — শুধু সবখানে একই রাখুন।)

এবার যাচাই আর pagination-এর আসল function দুটি চালিয়ে দেখুন। এগুলো সাধারণ JavaScript, তাই ব্রাউজারেই চলে — আর ঠিক এই function-ই Express-এর handler থেকে ডাকা হবে:

const BD_PHONE = /^01[3-9]\d{8}$/;
function validateOrder(body) {
  const fields = {};
  if (!BD_PHONE.test(body?.phone ?? "")) fields.phone = "১১ অঙ্কের সঠিক মোবাইল নম্বর দিন";
  if (!Array.isArray(body?.items) || body.items.length === 0) fields.items = "অন্তত একটি পণ্য দিন";
  else body.items.forEach((it, i) => {
    if (!Number.isInteger(it?.qty) || it.qty < 1 || it.qty > 20) fields[`items.${i}.qty`] = "পরিমাণ ১ থেকে ২০";
  });
  if (Object.keys(fields).length === 0) return null;
  return { status: 422, error: { code: "VALIDATION_FAILED", message: "কিছু তথ্য ঠিক নেই", fields } };
}
function paginate(list, q) {
  const page = Math.max(1, parseInt(q.page) || 1);
  const limit = Math.min(50, Math.max(1, parseInt(q.limit) || 10));
  const sorted = q.sort === "-price" ? list.toSorted((a, b) => b.price - a.price) : list;
  const data = sorted.slice((page - 1) * limit, page * limit).map((p) => p.id);
  return { data, page, limit, total: list.length, totalPages: Math.ceil(list.length / limit) };
}
console.log(JSON.stringify(validateOrder({ phone: "0171234", items: [{ id: 7, qty: 0 }] })));
console.log(validateOrder({ phone: "01712345678", items: [{ id: 7, qty: 2 }] }));
const rice = Array.from({ length: 23 }, (_, i) => ({ id: i + 1, price: 50 + ((i * 37) % 60) }));
console.log(JSON.stringify(paginate(rice, { page: "2", limit: "5", sort: "-price" })));
const { data, ...meta } = paginate(rice, { page: "abc", limit: "1000" });
console.log(data.length, meta);

প্রথম লাইনে পুরো error envelope — দুটি field-এর ভুল একসাথে। দ্বিতীয় লাইনে null: ঠিক অর্ডারে কোনো ভুল নেই। তৃতীয় লাইনে দাম অনুযায়ী বেশি থেকে কম সাজিয়ে দ্বিতীয় পাতার পাঁচটি id আর "totalPages":5. শেষ লাইন 23 { page: 1, limit: 50, total: 23, totalPages: 1 } — দুষ্টু query নিরাপদে সামলানো। দুটি সূক্ষ্ম বিষয়: toSorted (ES2023) মূল তালিকা বদলায় না, তাই পরের অনুরোধে ক্রম গুলিয়ে যায় না; আর sort-এর মান একটি নির্দিষ্ট তালিকা থেকেই নেওয়া হয়েছে — ব্যবহারকারীর দেওয়া field-নাম সরাসরি ডেটাবেসের query-তে বসানো বিপজ্জনক (পর্ব ৫২)। খুব বড় বা দ্রুত বদলানো তালিকায় offset-এর বদলে cursor pagination ভালো: “শেষ দেখা id-র পর থেকে ১০টি” — নতুন পণ্য যোগ হলেও পাতা সরে যায় না।

৭ · Error middleware, async error আর নিরাপত্তার মূল নিয়ম

চার প্যারামিটারের error handler

Express একটি middleware-কে error handler হিসেবে চেনে শুধু তার প্যারামিটারের সংখ্যা দেখে — ঠিক চারটি: (err, req, res, next). ব্যবহার না করলেও next লিখতে হবে, নইলে Express এটিকে সাধারণ middleware ভাববে। এটি বসে সব route-এর পরে, একদম শেষে:

export class HttpError extends Error {          // পর্ব ২৯-এর custom error
  constructor(status, code, message, fields) {
    super(message);
    Object.assign(this, { status, code, fields });
  }
}

export function notFound(req, res) {            // কোনো route মেলেনি
  res.status(404).json({ error: { code: "NOT_FOUND", message: "এই ঠিকানায় কিছু নেই" } });
}

export function errorHandler(err, req, res, next) {
  if (res.headersSent) return next(err);        // উত্তর অর্ধেক গেছে — Express-কে দিন
  const status = err.status ?? err.statusCode ?? 500;
  if (status >= 500) console.error(err);        // পুরো বিবরণ শুধু সার্ভারের log-এ
  res.status(status).json({ error: {
    code: err.code ?? (status >= 500 ? "INTERNAL" : "BAD_REQUEST"),
    message: status >= 500 ? "সার্ভারে একটি সমস্যা হয়েছে, একটু পরে চেষ্টা করুন" : err.message,
    ...(err.fields && { fields: err.fields }),
  } });
}

// app.js-এর একদম শেষে:  app.use(notFound);  app.use(errorHandler);

এখন যেকোনো জায়গা থেকে throw new HttpError(404, "NOT_FOUND", "অর্ডারটি পাওয়া যায়নি") লিখলেই এক ছাঁচের উত্তর। express.json()-এর নিজের error-এও status থাকে (400, 413), তাই সেগুলোও ঠিক status পায়। আর অপ্রত্যাশিত সবকিছু হয় 500, যেখানে ব্যবহারকারী আসল বার্তা দেখেন না — কারণ "connect ECONNREFUSED 10.0.3.7:5432" বা SQL-এর টুকরো আক্রমণকারীকে আপনার ভেতরের নকশা জানিয়ে দেয়। Express-এর নিজের ডিফল্ট handler-ও NODE_ENV=production না থাকলে HTML-এ পুরো stack trace দেখায় — তাই প্রোডাকশনে এটি দিতে ভুলবেন না।

Async error — Express 5 বনাম Express 4

Handler সাধারণত async function — ডেটাবেসের জন্য অপেক্ষা করে। Async function-এর ভেতরে throw হলে সেটি আসলে একটি rejected Promise. Express 5 handler-এর ফেরত দেওয়া Promise reject হলে নিজেই next(err) ডাকে — উপরের কোড তাই নিরাপদ। Express 4 এটি করে না: rejected Promise কেউ ধরে না, client-এর অনুরোধ ঝুলে থাকে, আর Node একটি unhandled rejection দেখায় — Node 15 থেকে যার ডিফল্ট ফল পুরো process বন্ধ হয়ে যাওয়া। Express 4-এর পুরোনো প্রজেক্টে তাই প্রতিটি async handler মোড়াতে হয়:

// শুধু Express 4-এ দরকার
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
router.get("/:id", wrap(async (req, res) => { /* ... */ }));
// অথবা একবারে: import "express-async-errors";  (Express-কে patch করে)

AI আর পুরোনো টিউটোরিয়াল প্রায়ই Express 4-এর যুগের কোড দেয় — npm i body-parser আর app.use(bodyParser.json()). Express 4.16 (২০১৭) থেকেই express.json() built-in, আলাদা package লাগে না। এমন কোড দেখলে বুঝবেন উৎসটি পুরোনো, তাই async error সামলানোর দিকেও আলাদা নজর দিন।

নিরাপত্তার মূল নিয়ম — শুরু থেকেই

import express from "express";
import helmet from "helmet";
import cors from "cors";
import { rateLimit } from "express-rate-limit";

export const app = express();
app.set("trust proxy", 1);                    // Nginx/হোস্টিং-এর proxy-র পেছনে থাকলে — আসল IP
app.use(helmet());                            // নিরাপত্তা header, X-Powered-By বাদ
app.use(cors({ origin: ["https://shop.example.com"], credentials: true }));  // নির্দিষ্ট origin
app.use("/api", rateLimit({ windowMs: 15 * 60 * 1000, limit: 300 }));      // ১৫ মিনিটে ৩০০
app.use(express.json({ limit: "10kb" }));     // অর্ডারের body এর চেয়ে বড় হয় না
app.use(logger);
app.use("/api/products", productsRouter);
app.use("/api/orders", requireAuth, ordersRouter);
app.use(notFound);
app.use(errorHandler);
  • helmet: এক লাইনে একগুচ্ছ security header বসায় (Strict-Transport-Security, X-Content-Type-Options, Content-Security-Policy…) আর Express-এর X-Powered-By: Express header সরিয়ে দেয়, যাতে আপনার প্রযুক্তি ঢাক পিটিয়ে জানানো না হয়।
  • CORS: শুধু আপনার নিজের front-end-এর origin-কে অনুমতি দিন। cors() একা দিলে Access-Control-Allow-Origin: * যায় — প্রকাশ্য, লগইনহীন API-র জন্য ঠিক আছে। কিন্তু cookie বা credentials লাগলে ব্রাউজার * মানে না, আর তখন “যে origin চাইছে তাকেই অনুমতি” (origin: true) দেওয়া মানে যেকোনো সাইটকে আপনার ব্যবহারকারীর পরিচয়ে আপনার API পড়তে দেওয়া। বিস্তারিত পর্ব ৪৩-এ।
  • Rate limit: একটি IP নির্দিষ্ট সময়ে কতটি অনুরোধ পাঠাতে পারবে; বেশি হলে 429. লগইন আর OTP পাঠানোর endpoint-এ আরও কড়া সীমা দিন (যেমন ১৫ মিনিটে ৫ বার) — পাসওয়ার্ড অনুমানের আক্রমণ আর আপনার SMS বিল দুটোই বাঁচবে।
  • Body size limit: ডিফল্ট ১০০ KB, আপনার দরকার অনুযায়ী আরও কমান; বেশি হলে 413. ফাইল আপলোড আলাদা route-এ, আলাদা সীমায়।
  • Error-এ গোপন তথ্য নয়: stack trace, SQL, ফাইলের path, library-র version — কিছুই উত্তরে নয়। Log-এও পাসওয়ার্ড, token বা পুরো কার্ড নম্বর লিখবেন না।

Authentication এক নজরে

“আপনি কে” প্রমাণ করা authentication; “আপনি কী করতে পারেন” ঠিক করা authorization. Express-এ লগইনের দুটি প্রধান পথ:

🍪 Session + cookie

লগইনের পর সার্ভার একটি এলোমেলো session id বানিয়ে নিজের কাছে (Redis/ডেটাবেসে) রাখে, আর ব্রাউজারকে দেয় HttpOnly, Secure, SameSite cookie-তে। JavaScript cookie পড়তে পারে না, তাই XSS-এ চুরি কঠিন; logout মানে সার্ভারে session মুছে ফেলা। একই সাইটের ওয়েব অ্যাপের জন্য সাধারণত সবচেয়ে সরল ও নিরাপদ। CSRF-এর দিকে নজর রাখতে হয় (পর্ব ৪৩)।

express-session

🎫 Bearer token (JWT)

সার্ভার একটি সই করা JWT দেয়; client প্রতিটি অনুরোধে Authorization: Bearer <token> পাঠায়। সার্ভারকে কিছু মনে রাখতে হয় না — মোবাইল অ্যাপ আর একাধিক সার্ভিসের জন্য সুবিধার। কিন্তু মেয়াদ শেষের আগে বাতিল করা কঠিন, তাই মেয়াদ ছোট রাখা আর refresh token-এর ব্যবস্থা লাগে।

jsonwebtoken, jose

যে পথই নিন, পাসওয়ার্ড কখনো সাধারণ লেখায় বা সাধারণ hash-এ (MD5, SHA-256) নয় — রাখুন ধীর, salt-যুক্ত password hashing দিয়ে: Argon2id, bcrypt, বা Node-এর built-in crypto.scrypt. ডেটাবেস ফাঁস হলেও তখন পাসওয়ার্ডগুলো উল্টে বের করা অত্যন্ত ব্যয়বহুল। পূর্ণ auth ব্যবস্থা (রেজিস্ট্রেশন, ইমেইল যাচাই, পাসওয়ার্ড রিসেট, OAuth দিয়ে “Google দিয়ে লগইন”) এই পর্বের বাইরে; বাস্তবে অনেক দল এর জন্য পরীক্ষিত library বা সেবা ব্যবহার করে।

লগইন থাকলেই অনুমতি নয়। GET /api/orders/:id handler-এ শুধু findById(req.params.id) করলে লগইন-করা যেকোনো ব্যবহারকারী id বদলে বদলে (A-101, A-102, A-103…) অন্যদের নাম, ফোন আর ঠিকানা পড়ে ফেলবেন। প্রতিটি object-এর জন্য মালিকানা যাচাই করুন: findOne({ id, userId: req.user.id }), না পেলে 404 — 403 দিলে অন্তত জানা হয়ে যায় যে ওই id-র অর্ডার আছে। এই ভুলের নাম BOLA, আর OWASP-এর API তালিকায় এটি এক নম্বরে।

৮ · পেশাদার প্রজেক্ট — config, গঠন, টেস্ট, ডক আর বিকল্প

পরিবেশ-নির্ভর config

Port, ডেটাবেসের ঠিকানা, গোপন key — এগুলো কোডে লেখা যায় না; আপনার ল্যাপটপ আর প্রোডাকশন সার্ভারে মান আলাদা, আর গোপন মান git-এ গেলে চিরকালের মতো ফাঁস। এগুলো আসে environment variable থেকে: process.env.PORT. স্থানীয়ভাবে একটি .env ফাইলে রেখে Node 20.6 থেকে node --env-file=.env server.js দিয়ে চালানো যায় (আগে এর জন্য dotenv package লাগত)। .env অবশ্যই .gitignore-এ, আর রিপোজিটরিতে থাকে শুধু মানহীন একটি .env.example. বেশিরভাগ হোস্টিং প্ল্যাটফর্ম নিজেই PORT ঠিক করে দেয়, তাই process.env.PORT ?? 3000. শুরুতেই প্রয়োজনীয় মানগুলো যাচাই করে না পেলে অ্যাপ চালু হতে দেবেন না — মাঝরাতে অর্ধেক-কাজ-করা সার্ভারের চেয়ে চালু না হওয়া সার্ভার ভালো।

প্রজেক্টের গঠন — স্তরে স্তরে

shop-api/
├── src/
│   ├── app.js              # middleware আর router জোড়া — listen নয় (টেস্টের জন্য)
│   ├── server.js           # app.listen(process.env.PORT)
│   ├── routes/             # ঠিকানা → controller  (orders.js, products.js)
│   ├── controllers/        # req পড়া, যাচাই, status আর JSON
│   ├── services/           # ব্যবসার নিয়ম: দাম, স্টক, অর্ডারের অবস্থা
│   ├── repositories/       # ডেটাবেসের সাথে কথা (পর্ব ৫২)
│   └── middleware/         # auth, errors, logger
├── test/
├── .env.example
└── package.json

এই layered architecture-এর মূল নিয়ম: প্রতিটি স্তর শুধু ঠিক নিচের স্তরকে চেনে। Service জানে না HTTP কী — সে শুধু “অর্ডার তৈরি করো, স্টক না থাকলে ConflictError” জানে। ফলে একই service কাল একটি CLI টুল বা queue worker থেকেও ডাকা যায়, আর ডেটাবেস বদলালে শুধু repository বদলায়। এই পর্বে ডেটা রাখা হয়েছে মেমরিতে (একটি Map বা array) — সার্ভার বন্ধ হলেই সব হারায়। পর্ব ৫২-এ repository-র পেছনে আসল ডেটাবেস বসবে, বাকি স্তরগুলো না বদলেই।

API টেস্ট — supertest আর Vitest

পর্ব ৪৮-এর Vitest দিয়েই API টেস্ট করা যায়। supertest library Express app-কে সরাসরি নিয়ে নকল HTTP অনুরোধ পাঠায় — কোনো port খুলতে হয় না। এজন্যই app.js আর server.js আলাদা:

import request from "supertest";
import { describe, it, expect } from "vitest";
import { app } from "../src/app.js";

const auth = { Authorization: "Bearer demo-token-123" };

describe("POST /api/orders", () => {
  it("লগইন ছাড়া 401", async () => {
    await request(app).post("/api/orders").send({}).expect(401);
  });
  it("ভুল ফোন নম্বরে 422 আর field-এর error", async () => {
    const res = await request(app).post("/api/orders").set(auth)
      .send({ phone: "123", items: [{ productId: 42, qty: 1 }] });
    expect(res.status).toBe(422);
    expect(res.body.error.fields).toHaveProperty("phone");
  });
  it("ঠিক অর্ডারে 201 আর Location", async () => {
    const res = await request(app).post("/api/orders").set(auth)
      .send({ phone: "01712345678", items: [{ productId: 42, qty: 2 }] });
    expect(res.status).toBe(201);
    expect(res.headers.location).toMatch(/^\/api\/orders\//);
  });
});
$ npx vitest run
 ✓ test/orders.test.js (3 tests)
   ✓ POST /api/orders > লগইন ছাড়া 401
   ✓ POST /api/orders > ভুল ফোন নম্বরে 422 আর field-এর error
   ✓ POST /api/orders > ঠিক অর্ডারে 201 আর Location
 Test Files  1 passed (1)
      Tests  3 passed (3)

প্রত্যাশিত আউটপুট উপরে (সময়ের লাইন বাদ)। Happy path-এর চেয়ে error-এর পথগুলোর টেস্ট বেশি জরুরি: ৪০১, ৪০৩, ৪২২, অন্যের অর্ডার চাইলে ৪০৪, খুব বড় body-তে ৪১৩।

ডকুমেন্টেশন আর versioning

Front-end-এর সহকর্মী বা অন্য কোম্পানি আপনার API ব্যবহার করবে কীভাবে? প্রচলিত মান হলো OpenAPI (আগের নাম Swagger): একটি YAML/JSON ফাইলে প্রতিটি endpoint, প্যারামিটার, body-র গঠন আর সম্ভাব্য status লেখা থাকে। সেখান থেকে Swagger UI-এর মতো টুল চেষ্টা করে দেখার মতো পাতা বানায়, আর client-এর কোডও তৈরি করা যায়। Zod-এর schema থেকে OpenAPI বানানোর library-ও আছে — তাহলে যাচাই আর ডক একই উৎস থেকে আসে, কখনো অমিল হয় না।

একবার মোবাইল অ্যাপ বাজারে গেলে আপনি আর পুরোনো API ভাঙতে পারবেন না — হাজারো ফোনে পুরোনো সংস্করণ চলছে। তাই API versioning: app.use("/api/v1/orders", ...). নতুন field যোগ করা ভাঙন নয়; field সরানো, নাম বদলানো বা অর্থ বদলানো ভাঙন — তখন /api/v2, আর পুরোনোটি কিছুকাল চালু রেখে ধীরে ধীরে বন্ধ।

Express-এর বিকল্প

Frameworkবৈশিষ্ট্যকখন ভাববেন
Expressছোট, নমনীয়, বিশাল ecosystem আর অগণিত টিউটোরিয়ালশেখা, বেশিরভাগ সাধারণ API
FastifyJSON Schema দিয়ে যাচাই ও দ্রুত serialization, plugin ব্যবস্থা, async শুরু থেকেইপারফরম্যান্স আর কড়া schema দরকার হলে
HonoWeb Standard-এর Request/Response-এর ওপর; Node, Deno, Bun আর Cloudflare Workers-এ একই কোডEdge/serverless পরিবেশ
NestJSTypeScript, decorator, dependency injection — Angular আর Spring-এর ধাঁচে কাঠামো; ভেতরে Express বা Fastifyবড় দল, বড় কোডবেস, কড়া কাঠামো চাইলে
KoaExpress-এর দলেরই বানানো; async middleware-এর “পেঁয়াজের খোসা” মডেল, খুব ছোট coreন্যূনতম core পছন্দ হলে

ধারণাগুলো — routing, middleware, যাচাই, error handler, status code — সবগুলোতে একই। Express-এ যা শিখলেন, তা যেকোনোটিতে নিয়ে যেতে পারবেন।

C/C++/Python/Java জানলে

Java-র Spring Boot-এ একই কাজ annotation দিয়ে হয়: @GetMapping("/{id}") হলো app.get("/:id"), @PathVariable হলো req.params, Bean Validation (@Valid) হলো আমাদের validateOrder, আর @RestControllerAdvice হলো error middleware — controller-service-repository স্তরও হুবহু এক (Spring Boot দিয়ে REST API). Python-এ Flask প্রায় Express-এর মতো ছোট, আর FastAPI type hint থেকেই যাচাই ও OpenAPI ডক বানায়, অনেকটা Express + Zod-এর মতো (ওয়েব ব্যাকএন্ড — FastAPI). C++-এ Crow বা Drogon-এর মতো framework আছে, যা খুব দ্রুত কিন্তু ecosystem ছোট। বড় পার্থক্য: Node-এ একটি thread আর event loop সব অনুরোধ সামলায়, তাই handler-এ ভারী CPU-র কাজ পুরো সার্ভার আটকে দেয় — Java বা Python-এর thread-ভিত্তিক সার্ভারে প্রভাবটা ভিন্ন।

পরীক্ষা ও ইন্টারভিউতে
  • Middleware কী? — (req, res, next) function, যা request পড়ে বা বদলায়, উত্তর দেয় অথবা next() দিয়ে পরের জনকে দেয়; ঘোষণার ক্রমে চলে।
  • Express error handler কীভাবে চেনে? — ঠিক চার প্যারামিটার (err, req, res, next); সব route-এর পরে বসাতে হয়; next(err) বা (Express 5-এ) rejected Promise সেখানে নিয়ে যায়।
  • req.params, req.query, req.body-এর পার্থক্য? — ঠিকানার :id অংশ; ?-এর পরের অংশ; অনুরোধের body (express.json() লাগে) — প্রথম দুটি সবসময় string.
  • PUT আর PATCH? POST কেন idempotent নয়? — PUT পুরোটা বদলে বসায়, PATCH অংশবিশেষ; POST প্রতিবার নতুন resource বানায়, তাই দুবার পাঠালে দুটি।
  • 401, 403, 404, 422 কখন? — পরিচয় নেই; অনুমতি নেই; নেই (বা অন্যের জিনিস লুকাতে); গঠন ঠিক কিন্তু মান ভুল।

৯ · AI-এর যুগে — AI-এর লেখা API যাচাই

“Express দিয়ে একটি অর্ডার API বানাও” — AI এক মিনিটে চল্লিশ লাইন দেবে, Postman-এ সব সবুজ। কারণ ডেমোতে কেউ দুষ্টু ইনপুট পাঠায় না, ডেটাবেস কখনো ভাঙে না, আর একজনই ব্যবহারকারী। AI-এর লেখা ব্যাকএন্ডে বারবার যে সমস্যাগুলো দেখা যায়:

AI যা প্রায়ই লেখেসমস্যাযা চাইবেন
Order.create(req.body), কোনো যাচাই নেইঋণাত্মক পরিমাণ, ১ টাকার দাম, role: "admin"schema দিয়ে যাচাই, শুধু চেনা field
সবকিছুতে res.json(...) বা res.send("error")error-ও 200; client, cache আর monitoring ভুল বোঝেসঠিক status, এক ছাঁচের error
res.status(500).json({ error: err.message, stack: err.stack })ভেতরের নকশা, SQL, path ফাঁসসাধারণ বার্তা; বিবরণ শুধু log-এ
bodyParser.json(), Express 4-এর ধাঁচপুরোনো উৎস; async error ধরা পড়ে নাExpress 5, express.json({ limit })
async handler-এ try/catch বা wrap নেই (Express 4-এ)অনুরোধ ঝুলে থাকে, process পড়ে যেতে পারেExpress 5, নয়তো wrap
password: req.body.password সোজা ডেটাবেসেফাঁস হলে সবার পাসওয়ার্ড প্রকাশ্যArgon2id/bcrypt hash
cors({ origin: true, credentials: true }) বা *যেকোনো সাইট ব্যবহারকারীর পরিচয়ে API পড়েনির্দিষ্ট origin-এর তালিকা
findById(req.params.id), মালিকানা যাচাই নেইid বদলে অন্যের তথ্য (BOLA)প্রতিটি object-এ userId যাচাই

AI-কে API লিখতে বললে ঝুঁকিগুলো শুরুতেই শর্ত করে দিন, আর টেস্টও চেয়ে নিন:

আমি Node.js 20 আর Express 5 (ESM) দিয়ে একটি অনলাইন মুদি দোকানের API বানাচ্ছি। এখন ডেটা মেমরিতে, পরে ডেটাবেস আসবে। নিচের শর্ত মেনে orders-এর অংশ লিখে দাও: ১. গঠন: routes/orders.js (express.Router), services/orderService.js, middleware/errors.js; app.js আর server.js আলাদা। ২. Endpoint: POST /api/orders, GET /api/orders/:id, PATCH /api/orders/:id/status (শুধু admin), DELETE /api/orders/:id. ৩. Status: তৈরি হলে 201 + Location, মুছলে 204, যাচাইয়ে ব্যর্থ হলে 422, না পেলে 404, লগইন নেই 401, অনুমতি নেই 403. ৪. সব error এই ছাঁচে: { error: { code, message, fields } }, message বাংলায়; 500-এ কখনো err.message বা stack নয়। ৫. Zod দিয়ে যাচাই: phone বাংলাদেশি মোবাইল (01 দিয়ে শুরু, ১১ অঙ্ক), items ১–২০টি, qty ১–২০; body-তে দাম বা userId এলে উপেক্ষা করবে। ৬. প্রতিটি অর্ডার পড়া, বদলানো, মোছার আগে যাচাই করবে অর্ডারটি req.user-এর কি না; না হলে 404. ৭. express.json({ limit: "10kb" }), helmet, নির্দিষ্ট origin-সহ cors, express-rate-limit; body-parser package ব্যবহার করবে না। শেষে supertest + Vitest দিয়ে ৬টি টেস্ট দাও: 401, 422, 201 + Location, অন্যের অর্ডারে 404, admin ছাড়া PATCH-এ 403, আর ১১ KB body-তে 413.

AI-এর লেখা API — যাচাইয়ের চেকলিস্ট

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Express প্রায় এক দশক ধরে নতুন বড় সংস্করণ ছাড়াই Node-এর সবচেয়ে জনপ্রিয় framework রইল কীভাবে? এই দীর্ঘ স্থবিরতা থেকে open source সম্পর্কে কী শেখার আছে?

Express-এর জন্ম Node-এর শৈশবে। ২০০৯ সালে Node প্রকাশের প্রায় সাথে সাথেই TJ Holowaychuk Ruby-র Sinatra দেখে অনুপ্রাণিত হয়ে Express লেখা শুরু করেন; ২০১০ সালে আসে প্রথম স্থিতিশীল সংস্করণ। নীচে ছিল Connect নামের middleware-স্তর, যাও TJ-দের হাতে তৈরি। দর্শনটি ছিল “ছোট core, বাকিটা middleware” — Rails-এর মতো সবকিছু ঠিক করে না দিয়ে শুধু routing আর pipeline. সময়টাও ছিল নিখুঁত: Node নতুন, সবাই একটি সহজ শুরু খুঁজছিল, আর Express-এর README পড়েই দশ মিনিটে সার্ভার চালানো যেত।

২০১৪ সালে Express 4 আসে — Connect-নির্ভরতা সরিয়ে নিজস্ব router. সে বছরই TJ Node ছেড়ে Go-র দিকে চলে যান এবং প্রজেক্টের দায়িত্ব StrongLoop নামের একটি কোম্পানির হাতে যায়, যা সম্প্রদায়ে বিতর্ক তোলে। পরে StrongLoop কিনে নেয় IBM, আর ২০১৬ সালে Express চলে যায় Node.js Foundation-এর (এখন OpenJS Foundation) অধীনে। এর মধ্যেই ২০১৪ সালে শুরু হয়েছিল Express 5-এর alpha — কিন্তু চূড়ান্ত প্রকাশ হতে লেগে যায় প্রায় দশ বছর, ২০২৪ সাল পর্যন্ত। এতদিন কাজ চালিয়েছেন অল্প কয়েকজন স্বেচ্ছাসেবক রক্ষণাবেক্ষণকারী।

তবু Express জনপ্রিয় রইল কেন? প্রথমত, স্থিতিশীলতাও একটি বৈশিষ্ট্য — ২০১৫ সালের টিউটোরিয়ালের কোড ২০২৩ সালেও চলত। দ্বিতীয়ত, network effect: হাজারো middleware, লাখো Stack Overflow উত্তর, প্রতিটি bootcamp-এর পাঠ্যক্রম — এবং এখন AI-এর প্রশিক্ষণ-ডেটাও। তৃতীয়ত, ছোট API মানে ভাঙার জায়গাও কম। কিন্তু দামও ছিল: async/await আসার পরও Express 4 rejected Promise ধরত না, আর path-matching library-তে regex-ভিত্তিক ReDoS-এর মতো দুর্বলতার খবরও এসেছে। Express 5 শেষে async error সামলানো, নতুন ও নিরাপদ path-নিয়ম আর পুরোনো Node-এর সমর্থন বাদ দেওয়া নিয়ে আসে। এর পর থেকে প্রজেক্টটি আরও নিয়মিত প্রকাশের পথে ফিরেছে।

মূল উপলব্ধি: একটি library-র জনপ্রিয়তা আর তার রক্ষণাবেক্ষণের স্বাস্থ্য আলাদা জিনিস। Dependency বাছাইয়ের সময় শুধু download সংখ্যা নয় — শেষ প্রকাশ কবে, কতজন রক্ষণাবেক্ষণকারী, নিরাপত্তা-সমস্যায় কত দ্রুত সাড়া দেয়, পেছনে কোনো foundation আছে কি না — এসবও দেখুন। আর যে open source প্রতিদিন ব্যবহার করেন, পারলে তাকে অবদান বা অর্থ দিয়ে টিকিয়ে রাখুন।

প্র ০২Roy Fielding-এর আসল REST আর আজকের “JSON over HTTP” API কি একই জিনিস? HATEOAS কী, আর প্রায় কেউ এটি মানে না কেন?

REST শব্দটি এসেছে ২০০০ সালে University of California, Irvine-এ জমা দেওয়া Roy Fielding-এর PhD গবেষণাপত্র থেকে। Fielding নিজে HTTP/1.1 স্পেসিফিকেশনের অন্যতম লেখক; তিনি বর্ণনা করতে চেয়েছিলেন ওয়েব কেন এত বড় হয়েও টিকে আছে। তাঁর REST একগুচ্ছ শর্ত: client-server বিভাজন; stateless অনুরোধ; উত্তর cache করা যাবে কি না তা স্পষ্ট; স্তরে স্তরে সাজানো সিস্টেম (মাঝে proxy, CDN); ঐচ্ছিকভাবে সার্ভার থেকে কোড পাঠানো; আর সবচেয়ে গুরুত্বপূর্ণ — uniform interface, যার একটি অংশ HATEOAS (Hypermedia As The Engine Of Application State).

HATEOAS মানে client আগে থেকে ঠিকানা মুখস্থ রাখবে না; প্রতিটি উত্তরের ভেতরেই থাকবে পরের সম্ভাব্য কাজের link — যেমন একটি অর্ডারের JSON-এ "cancel": "/api/orders/A-101" শুধু তখনই থাকবে, যখন বাতিল করা সম্ভব। একটি ওয়েব পাতা ঠিক এভাবেই চলে: আপনি ব্রাউজারে কোনো URL মুখস্থ করেন না, link আর ফর্ম ধরে এগোন। সার্ভার URL বদলালেও ব্রাউজার ভাঙে না।

আজ “REST API” নামে যা চলে, তার বেশিরভাগ — এই পর্বের API-ও — আসলে “REST-ধাঁচের JSON API”: resource, method আর status code মানে, কিন্তু hypermedia নয়। Client-এর কোডে ঠিকানাগুলো লেখা থাকে, ডক থেকে নেওয়া। Leonard Richardson-এর বহুল আলোচিত “maturity model”-এ এটি স্তর ২; স্তর ৩ হলো hypermedia. Fielding নিজে ২০০৮ সালে একটি লেখায় স্পষ্ট বলেছিলেন, hypertext-চালিত না হলে সেটিকে REST বলা উচিত নয়।

তাহলে সবাই HATEOAS এড়ায় কেন? কারণ API-র client প্রায়ই মানুষ-চালিত ব্রাউজার নয়, একটি নির্দিষ্ট অ্যাপ, যা নির্দিষ্ট পর্দার জন্য নির্দিষ্ট ডেটা চায়; “link খুঁজে খুঁজে এগোনো” client লেখা কঠিন আর লাভ কম মনে হয়। আবার stateless আর cacheable-এর মতো শর্তগুলো সবাই মানে, কারণ সেগুলোর লাভ সরাসরি — যেকোনো সার্ভার যেকোনো অনুরোধ নিতে পারে, CDN উত্তর জমাতে পারে।

মূল উপলব্ধি: নামের বিশুদ্ধতার চেয়ে প্রতিটি শর্ত কী সমস্যা সমাধান করে তা বোঝা জরুরি। Stateless কেন — যাতে সার্ভার বাড়ানো যায়; cacheable কেন — যাতে নেটওয়ার্ক বাঁচে; hypermedia কেন — যাতে client আর server আলাদাভাবে বদলাতে পারে। যে শর্তের লাভ আপনার দরকার, সেটি সচেতনভাবে নিন।

প্র ০৩Middleware-এর ধারণা কোথা থেকে এলো? Express-এর “শিকল” আর Koa-র “পেঁয়াজের খোসা”-র পার্থক্য কী, আর এর পেছনের design pattern কোনটি?

ধারণাটি পুরোনো। ১৯৯৪ সালে “Gang of Four”-এর বিখ্যাত Design Patterns বইয়ে একটি pattern আছে — Chain of Responsibility: একটি অনুরোধ কয়েকটি handler-এর শিকল ধরে যায়; প্রত্যেকে ঠিক করে নিজে সামলাবে নাকি পরের জনকে দেবে। পাঠানো পক্ষ জানেই না শেষ পর্যন্ত কে সামলাল। Java-র Servlet API-তে filter (২০০১ সালের দিকে), Python-এর WSGI (২০০৩)-এর middleware — সবই এই চিন্তা।

ওয়েব framework-জগতে এটিকে সবচেয়ে প্রভাবশালী রূপ দেয় Ruby-র Rack (২০০৭): প্রতিটি অ্যাপ বা middleware একটি object, যার একটি call(env) method আছে আর সেটি status, header আর body ফেরত দেয়। একটি middleware ভেতরের অ্যাপকে ডাকে, তার উত্তর পেয়ে চাইলে বদলায় — ফলে ছোট ছোট টুকরো জুড়ে বড় সার্ভার বানানো যায়। Node-জগতে ২০১০ সালে Sencha Labs-এর Connect একই চিন্তা নিয়ে আসে, তবে callback-এর ভাষায়: function (req, res, next). Express প্রথমে Connect-এর ওপরই বানানো ছিল।

Express-এর মডেল একমুখী শিকল: next() ডাকলে পরের জন চলে, কিন্তু সে কখন শেষ করল তা জানতে চাইলে res.on("finish")-এর মতো event-এ ভরসা করতে হয় (এই পর্বের logger). ২০১৩ সালে Express-এর দলই বানায় Koa, যেখানে middleware async function, আর await next() লিখলে পরের সবাই কাজ শেষ করার পর নিয়ন্ত্রণ আবার আপনার কাছে ফেরে। ছবিটা পেঁয়াজের খোসার মতো — অনুরোধ বাইরে থেকে ভেতরে ঢোকে, উত্তর ভেতর থেকে বাইরে বেরোয়, আর প্রতিটি স্তর দুবার সুযোগ পায়। সময় মাপা বা পুরো উত্তরকে মোড়ানো তাই সেখানে স্বাভাবিক: const t = Date.now(); await next(); log(Date.now() - t).

একই pattern আজ সবখানে: ASP.NET Core-এর pipeline, Redux-এর middleware, Axios-এর interceptor, এমনকি অনেক AI-agent framework-এর “hook” — সবই অনুরোধকে ধাপে ধাপে বদলানোর শিকল। দুর্বলতাও একই: শিকল লম্বা হলে কে কোন ধাপে req-এ কী বসাল, তা খুঁজে পাওয়া কঠিন হয়, আর ক্রমের ভুল নীরবে নিরাপত্তা-ফাঁক তৈরি করে।

মূল উপলব্ধি: Middleware হলো cross-cutting কাজ — log, auth, compression, error — কে ব্যবসার কোড থেকে আলাদা করার উপায়। কিন্তু শিকলের প্রতিটি কড়া ক্রমের ওপর নির্ভরশীল; তাই middleware-এর তালিকা একটি নিরাপত্তা-নথির মতো পড়ুন আর টেস্ট দিয়ে পাহারা দিন।

প্র ০৪REST, GraphQL আর gRPC — একটি নতুন প্রজেক্টে কোনটি বাছবেন, আর কেন?

তিনটিই client আর server-এর কথোপকথনের তিন রকম চুক্তি। REST-এ অনেকগুলো ঠিকানা, প্রতিটি একটি resource; সার্ভার ঠিক করে কী ফেরত যাবে। এর বড় শক্তি HTTP-র সাথে পুরো মিল — GET উত্তর ব্রাউজার আর CDN নিজেই cache করে, status code সবাই বোঝে, curl দিয়েই পরীক্ষা করা যায়। দুর্বলতা: একটি পর্দার জন্য কখনো বেশি ডেটা আসে (over-fetching), কখনো কয়েকটি অনুরোধ লাগে (under-fetching) — যেমন অর্ডার, তারপর প্রতিটি পণ্য, তারপর ডেলিভারির অবস্থা।

GraphQL Facebook-এ তৈরি হয় ২০১২ সালে, মূলত মোবাইল অ্যাপের news feed-এর এই সমস্যার জন্য, আর প্রকাশ্যে আসে ২০১৫ সালে। এখানে সাধারণত একটি endpoint; client একটি query-তে ঠিক যে field-গুলো চায় তা লিখে পাঠায়, আর একটি strongly-typed schema চুক্তি হিসেবে কাজ করে। লাভ: এক অনুরোধে ঠিক দরকারি ডেটা, front-end দল স্বাধীন। দাম: HTTP cache প্রায় কাজে আসে না (বেশিরভাগ query POST-এ যায়), একটি জটিল query সার্ভারকে বসিয়ে দিতে পারে বলে query-র গভীরতা ও খরচের সীমা লাগে, আর প্রতিটি field-এ আলাদা ডেটাবেস-অনুরোধের “N+1” সমস্যা সামলাতে বাড়তি কৌশল লাগে।

gRPC Google প্রকাশ করে ২০১৫ সালে, তাদের ভেতরের একটি RPC ব্যবস্থার উত্তরসূরি হিসেবে। এটি RPC ধাঁচের — resource নয়, function ডাকা: CreateOrder(req). ডেটা যায় Protocol Buffers নামের compact binary ফরম্যাটে, HTTP/2-এর ওপর, দুদিকে streaming-সহ; .proto ফাইল থেকে অনেক ভাষার client কোড তৈরি হয়। ফলে microservice-দের নিজেদের মধ্যে কথায় খুব দ্রুত আর type-নিরাপদ। কিন্তু ব্রাউজার সরাসরি gRPC বলতে পারে না (gRPC-Web আর একটি proxy লাগে), আর binary বার্তা চোখে পড়া যায় না।

বাস্তব পছন্দ প্রায়ই মিশ্র: প্রকাশ্য বা third-party API REST-এ (সবাই বোঝে), নানা পর্দার জটিল front-end-এর জন্য GraphQL স্তর, আর ভেতরের সার্ভিসগুলোর মধ্যে gRPC. TypeScript-এর একই repo-তে front আর back থাকলে tRPC-র মতো পথও জনপ্রিয় — type সরাসরি ভাগ হয়, আলাদা schema লাগে না।

মূল উপলব্ধি: প্রশ্নটা “কোনটা আধুনিক” নয়, “client কারা, কতজন, আর কে কাকে বদলাতে পারে”। ছোট দল আর সাধারণ CRUD-এ REST প্রায় সবসময় যথেষ্ট — আর ভালোভাবে বানানো REST, খারাপভাবে বানানো GraphQL-এর চেয়ে অনেক ভালো।

প্র ০৫OWASP-এর API নিরাপত্তা তালিকায় এক নম্বর ঝুঁকি “Broken Object Level Authorization” কেন — অথচ এটি এত সহজ একটি ভুল?

OWASP ওয়েব অ্যাপের ঝুঁকির বিখ্যাত Top 10 তালিকা প্রকাশ করে আসছে বহু বছর ধরে। কিন্তু মোবাইল অ্যাপ আর single-page অ্যাপের যুগে দেখা গেল, API-র ঝুঁকির ধরন আলাদা — তাই ২০১৯ সালে আসে আলাদা OWASP API Security Top 10, আর ২০২৩ সালে তার হালনাগাদ সংস্করণ। দুই সংস্করণেই এক নম্বরে BOLA — Broken Object Level Authorization.

ভুলটি হাস্যকর রকম সহজ: GET /api/orders/A-101 handler লগইন যাচাই করে, তারপর findById("A-101") — কিন্তু অর্ডারটি এই ব্যবহারকারীর কি না, তা আর দেখে না। আক্রমণকারীর কোনো বিশেষ টুল লাগে না, শুধু id বদলাতে হয়। id যদি ক্রমিক সংখ্যা হয় (১০১, ১০২…), তাহলে একটি সাধারণ লুপেই পুরো গ্রাহক-তালিকা — নাম, ফোন, ঠিকানা — বেরিয়ে আসে।

এত সহজ হয়েও এটি এক নম্বরে কেন? কারণ এটি কোনো framework ধরতে পারে না। SQL injection ঠেকাতে parameterized query আছে, XSS ঠেকাতে auto-escaping — কিন্তু “এই object-টি এই ব্যবহারকারী দেখতে পারবেন কি না” একটি ব্যবসার নিয়ম, যা প্রতিটি endpoint-এ নতুন করে লিখতে হয়। দশটি endpoint-এর নয়টিতে মনে থাকলেও একটিতে ভুললেই যথেষ্ট। Automated scanner-ও এটি সহজে ধরে না, কারণ উত্তরটি দেখতে একদম স্বাভাবিক — 200 আর বৈধ JSON. আর API যত বেশি id প্রকাশ করে, ঝুঁকিও তত বাড়ে।

২০২৩-এর তালিকায় আরও আছে — object-এর কোন field কে পড়তে বা লিখতে পারবে (Broken Object Property Level Authorization, যার মধ্যে mass assignment পড়ে), সীমাহীন resource খরচ (আমাদের limit-এর উপরের সীমা আর rate limit), function-স্তরের অনুমতি (অ্যাডমিনের endpoint সাধারণ ব্যবহারকারী ডাকতে পারা), আর SSRF-এর মতো ঝুঁকি। প্রতিরোধের মূল কৌশল: মালিকানার শর্ত query-র ভেতরেই রাখা (WHERE id = ? AND user_id = ?), অনুমতির যাচাই এক জায়গায় কেন্দ্রীভূত করা, অনুমানযোগ্য ক্রমিক id-র বদলে UUID-এর মতো id (এটি একা সমাধান নয়, শুধু বাড়তি স্তর), আর প্রতিটি endpoint-এর জন্য “অন্য ব্যবহারকারীর token দিয়ে চাইলে ৪০৪” ধরনের টেস্ট।

মূল উপলব্ধি: Authentication একবার, প্রবেশদ্বারে; authorization প্রতিটি object-এ, প্রতিবার। সবচেয়ে বিপজ্জনক নিরাপত্তা-ভুলগুলো প্রায়ই কোনো জটিল কৌশল নয় — একটি ভুলে যাওয়া AND user_id = ?.

অনুশীলন

বেশিরভাগ কাজে কোড লিখতে হবে না — নকশা নিয়ে ভাবা আর কোড পড়াই আসল কাজ।

  1. Endpoint নকশা: একটি কোচিং সেন্টারের API-র জন্য endpoint আর সফল/ব্যর্থ status লিখুন: সব ব্যাচের তালিকা (বিষয় অনুযায়ী ছাঁকা, পাতায় পাতায়), একটি ব্যাচ, ব্যাচে ভর্তি হওয়া, একজন শিক্ষার্থীর ভর্তি বাতিল, আর ব্যাচের ফি বদলানো (শুধু অ্যাডমিন)।
    • GET /api/batches?subject=physics&page=1&limit=20 → 200
    • GET /api/batches/:id → 200 / 404
    • POST /api/batches/:id/enrollments → 201 + Location: /api/enrollments/E-55; আসন পূর্ণ বা আগেই ভর্তি হলে 409; ফোন ভুল হলে 422; লগইন না থাকলে 401
    • DELETE /api/enrollments/:id → 204; অন্যের ভর্তি হলে 404
    • PATCH /api/batches/:id body { "fee": 2500 } → 200; অ্যাডমিন না হলে 403; ঋণাত্মক ফি-তে 422

    খেয়াল করুন: ঠিকানায় কোনো ক্রিয়া নেই (/enroll, /cancelEnrollment নয়) — ভর্তি নিজেই একটি resource.

  2. ক্রমের বাগ: একজন লিখলেন — app.get("/api/products/:id", getOne); app.use(express.json()); app.get("/api/products/featured", featured); app.post("/api/products", create);. দুটি সমস্যা খুঁজুন।

    (১) /api/products/featured কখনো পৌঁছাবে না — আগের :id route সেটিকে { id: 'featured' } হিসেবে ধরে ফেলে, আর getOne সম্ভবত 404 দেবে। সমাধান: নির্দিষ্ট route আগে লিখুন।

    (২) এখানে express.json() আসলে create-এর আগেই আছে, তাই POST কাজ করবে — কিন্তু এটি ভঙ্গুর: কেউ কোনো POST route ওপরে সরালে সেখানে req.body থাকবে না। নিয়ম: body parser আর সাধারণ middleware সব route-এর আগে, একদম শুরুতে; error handler একদম শেষে।

  3. Error handler পরীক্ষা: উপরের চালানো যায় এমন mini-express প্রোগ্রামে একটি route যোগ করুন: route("GET", "/api/boom", () => { throw new Error("DB password=secret"); }), আর অনুরোধের তালিকায় ["GET", "/api/boom"]. কী আউটপুট আশা করেন? তারপর একটি ভাঙা JSON (body: "{oops") পাঠালে কী হবে?

    /api/boom-এ পাবেন 500 {"error":"সার্ভারে সমস্যা"}. Error-টি Promise.resolve().then(...)-এর ভেতরে ছোড়া হয়েছে, তাই .catch(next) সেটি ধরে error handler-এ দেয়; error-এ status নেই, তাই ৫০০ আর সাধারণ বার্তা — “password=secret” ব্যবহারকারীর কাছে যায়নি। ভাঙা JSON-এ parser-এর catch চলবে: 400 {"error":"ভাঙা JSON"}. (খেয়াল করুন, mini-express-এ ৫০০-এর আসল error কোথাও log হয় না — বাস্তবে সেটি অবশ্যই সার্ভারের log-এ লিখতে হবে।)

  4. AI অডিট: যেকোনো AI-কে বলুন “Express দিয়ে একটি user registration আর login API লেখো”। এই পর্বের চেকলিস্ট ধরে যাচাই করুন। কোথায় কোথায় সমস্যা পেলেন?

    সাধারণত যা দেখা যায়:

    • পাসওয়ার্ড সোজা সংরক্ষণ, বা বড়জোর SHA-256 — ধীর, salt-যুক্ত hash (Argon2id/bcrypt) নয়।
    • ইমেইল আগেই থাকলে 400 বা 500 — হওয়ার কথা 409; আর লগইনে “ইমেইল পাওয়া যায়নি” বনাম “পাসওয়ার্ড ভুল” আলাদা বার্তা, যা দিয়ে কোন ইমেইল নিবন্ধিত তা জেনে নেওয়া যায় — একটিই সাধারণ বার্তা দিন।
    • JWT-র secret কোডেই লেখা ("secret123"), মেয়াদ নেই বা অনেক লম্বা।
    • লগইন endpoint-এ rate limit নেই — পাসওয়ার্ড অনুমানের আক্রমণ খোলা।
    • catch-এ res.status(500).json(err) — ভেতরের বিবরণ ফাঁস।

    প্রতিটির জন্য AI-কে নির্দিষ্টভাবে বলুন কী বদলাতে হবে, আর টেস্ট চেয়ে নিন।

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

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

ক্রমএকটি Express অ্যাপে middleware আর route সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🛡️ app.use(helmet()) — নিরাপত্তা header
  2. 🧾 app.use(express.json({ limit: "10kb" })) — body পড়া
  3. 🔑 app.use("/api/orders", requireAuth, ordersRouter) — লগইনসহ route
  4. 🔍 app.use(notFound) — কোনো route মেলেনি, ৪০৪
  5. 🚑 app.use(errorHandler) — চার প্যারামিটারের error handler

Express-এ app.get("/api/products/:id", handler) route-এ GET /api/products/42 এলে req.params.id-এর মান কী?

  • সংখ্যা 42
  • string '42'
  • undefined, কারণ query string নেই
  • { id: 42 } object

একটি Express middleware next() ডাকল না, আবার উত্তরও পাঠাল না। কী ঘটবে?

  • Express নিজেই পরের middleware চালাবে
  • Client সাথে সাথে 404 পাবে
  • অনুরোধ ঝুলে থাকবে — client উত্তরের অপেক্ষায় থাকবে
  • Node process বন্ধ হয়ে যাবে

Express কোন বৈশিষ্ট্য দেখে একটি middleware-কে error-handling middleware হিসেবে চেনে?

  • Function-এর নাম errorHandler হলে
  • ফাইলটি middleware/errors.js-এ থাকলে
  • সবচেয়ে শেষে app.use করলেই
  • ঠিক চারটি প্যারামিটার থাকলে (err, req, res, next)

একটি REST API-তে নতুন অর্ডার সফলভাবে তৈরি হলো। সবচেয়ে উপযুক্ত উত্তর কোনটি?

  • 201 Created, Location: /api/orders/A-101, body-তে নতুন অর্ডার
  • 200 OK, body-তে { "success": true }
  • 204 No Content
  • 302 Found, অর্ডারের পাতায় redirect

JSON ঠিকভাবে parse হয়েছে, কিন্তু অর্ডারে qty: 0 আর ভুল ফোন নম্বর। প্রচলিত রীতিতে কোন status দেওয়া উচিত?

  • 400 Bad Request
  • ৪২২, field ধরে ধরে error-সহ
  • 409 Conflict
  • 500 Internal Server Error

লগইন-করা একজন ব্যবহারকারী URL-এর id বদলে অন্য গ্রাহকের অর্ডার দেখতে পারছেন। এই দুর্বলতার নাম আর সমাধান কী?

  • CORS-এর ভুল — origin সীমিত করা
  • Rate limit নেই — rate limiter যোগ করা
  • BOLA — প্রতিটি অর্ডার পড়ার সময় মালিকানা (userId) যাচাই
  • HTTPS নেই — certificate বসানো

শব্দকোষ

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

মনে রাখুন

  1. REST API ডেটাকে resource হিসেবে URL-এ সাজায়; কাজ বলে method, ফল বলে status code আর JSON.
  2. Express = routing + middleware; প্রতিটি request (req, res, next)-এর শিকল ধরে ঘোষণার ক্রমে চলে।
  3. req.params আর req.query সবসময় string; req.body-র জন্য express.json() লাগে — আর তিনটিই সার্ভারে যাচাই করতে হয়।
  4. সঠিক status (২০১ + Location, ২০৪, ৪২২, ৪০৪…) আর এক ছাঁচের error; চার প্যারামিটারের error handler সবার শেষে, ৫০০-তে গোপন তথ্য নয়।
  5. helmet, নির্দিষ্ট origin-এর CORS, rate limit, body-র সীমা, hash করা পাসওয়ার্ড — আর প্রতিটি object-এ মালিকানা যাচাই।

পরের পর্বে: REST-এ client জিজ্ঞেস করে, সার্ভার উত্তর দেয়। কিন্তু অর্ডারের অবস্থা বদলালে সার্ভার নিজে থেকে client-কে জানাবে কীভাবে? WebSocket আর Server-Sent Events দিয়ে real-time যোগাযোগ।

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

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