এই পর্বে যা শিখবেন
- একটি request কীভাবে middleware-এর সারি পার হয়, আর
(req, res, next)-এর আসল অর্থ - app, router আর route, এই তিন স্তরে middleware বসানো এবং ক্রম ঠিক করা
express.json,express.urlencoded,express.staticআর তাদের সীমা- CORS, preflight আর credentials, সত্যিকারের browser ও curl আউটপুট দেখে
- নিজের timer, logger আর auth check লেখা, chain থামানো, আর ঝুলে থাকা request ধরা
১ · পাইপলাইন — একটি request-এর সারি পার হওয়া
ডাকঘরে একটি পার্সেল এলে কী হয় ভাবুন। প্রথম টেবিলে সিল পড়ে, কখন এলো তা লেখা হয়। দ্বিতীয় টেবিলে ঠিকানা যাচাই হয়। তৃতীয় টেবিলে ওজন মাপা হয়, বেশি ভারী হলে সেখানেই ফেরত। চতুর্থ টেবিলে দেখা হয় মাশুল দেওয়া আছে কি না। সব পার হলে তবেই পার্সেলটি বাছাই ঘরে যায়।
প্রতিটি টেবিলের কাজ ছোট আর আলাদা। কেউ জানে না তার পরের টেবিলে ঠিক কী হবে। আবার যেকোনো টেবিল পার্সেলটি থামিয়ে ফেরত পাঠাতে পারে। পর্ব ০২-এ অনুরোধের যাত্রায় app server-এর ভেতরে যে “middleware” স্তরটি দেখেছিলেন, সেটি ঠিক এমনই। পর্ব ২৫-এ route লিখেছেন; এবার সেই route-এর সামনের টেবিলগুলো সাজানোর পালা।
Middleware হলো এমন একটি ফাংশন, যা request আর শেষ handler-এর মাঝখানে বসে। সে req আর res পড়তে ও বদলাতে পারে, উত্তর পাঠিয়ে থামাতে পারে, অথবা next() ডেকে পরেরজনকে কাজ দিতে পারে। Express-এর নিজের docs-ও বলে, একটি Express app আসলে middleware ডাকার একটি ধারাবাহিক সারি।
Node-এ এই ধাঁচ জনপ্রিয় হয় ২০১০ সালের দিকে, Connect আর তার ওপর TJ Holowaychuk-এর লেখা Express দিয়ে। Express 5 বহু বছরের অপেক্ষার পর ২০২৪ সালে stable হয়। এই পর্বের সব সত্যিকারের run লেখার সময়ের version-এ: Express 5.3.0, cors 2.8.6, helmet 8.3.0, compression 1.8.2, express-rate-limit 8.7.2, Node 24.15. Version বদলায়, তাই নিজের package.json দেখে মিলিয়ে নেবেন।
একটি request-এর পাইপলাইন
Request in, through the chain, response back
নিচের ডায়াগ্রামে বাজারঘরের একটি সত্যিকারের অনুরোধ অনুসরণ করুন। POST /api/orders-এ JSON body আর token গেছে। প্রতিটি ধাপে req আর res-এর কোন মান বদলাল, ডান দিকের ঘরে দেখুন।
- Request
POSTএলো - Timer
startসময় নিল - cors
headerঅনুমতি - json
bodyপড়ল - Auth
userচিনল - Handler
201উত্তর
| req.body | |
| req.user | |
| Allow-Origin | |
| statusCode |
- Request এলো। Node একটি
reqআর একটিresবানাল। Body এখনো network-এর টুকরো হয়ে আছে, তাইreq.bodyহলোundefined. কে পাঠিয়েছে, তাও এখনো জানা নেই। - requestTimer. শুরুর সময় একটি ভ্যারিয়েবলে রাখল,
finishevent-এ একটি log লিখবে বলে জানিয়ে রাখল, তারপরnext()ডাকল।reqবদলায়নি, কিন্তু উত্তর শেষে কী হবে তা ঠিক হয়ে গেল। - cors.
Originheader তালিকায় আছে, তাই উত্তরের জন্যAccess-Control-Allow-Originবসাল। উত্তর এখনো পাঠানো হয়নি, শুধু header জমা হয়েছে। এটি preflight নয়, তাই সেnext()ডাকল। - express.json. Stream থেকে body-র সব টুকরো পড়ে JSON parse করল। এবার
req.body-তে{ item: "চাল" }এসেছে। এর পরের সবাই এই মান পাবে। - requireAuth. Token মিলেছে, তাই
req.userবসিয়েnext()ডাকল। আগের middleware যা বসিয়েছে পরের জন তা পায়, এটাই middleware-দের কথা বলার সাধারণ উপায়। - Handler.
req.bodyআরreq.userতৈরি অবস্থায় পেল।res.status(201).json(...)ডেকে উত্তর পাঠাল এবংnext()ডাকল না। সারি এখানেই শেষ। - উত্তর শেষ, log লেখা হলো। শেষ byte চলে যাওয়ার পর
finishevent চলল, আর ধাপ ২-এর timer সময় হিসাব করে log লিখল। এই 9.5ms এই লেখকের মেশিনে একবারের মাপ, সাধারণ ফল নয়।
২ · (req, res, next) — তিনটি অক্ষর নয়, তিনটি সিদ্ধান্ত
প্রতিটি middleware-এর আকার একই: তিনটি parameter নেওয়া একটি ফাংশন। req হলো আসা অনুরোধ, res হলো যাওয়া উত্তর, আর next হলো “আমার কাজ শেষ, পরের টেবিলে যান” বলার ফাংশন।
| Middleware যা করে | ফল |
|---|---|
next() | সারির পরের matching ফাংশন চলে |
res.json(...) বা res.send(...) | উত্তর গেল, chain থামল; পরের কেউ চলে না |
next(err) বা throw | স্বাভাবিক সারি ছেড়ে error middleware-এ যায় (পর্ব ২৮) |
next("route") | এই route-এর বাকি ফাংশন বাদ, পরের matching route-এ যায় |
next("router") | পুরো router থেকে বেরিয়ে app-এ ফেরে |
| কিছুই না | Request ঝুলে থাকে (অংশ ৮) |
Wandbox-এ Express import করা যায় না, তাই নিচের ব্লকে শেখার মডেল হিসেবে ৮ লাইনের একটি runner লেখা হয়েছে। next আসলে কত সাধারণ, সেটা দেখাই উদ্দেশ্য। চালিয়ে দেখুন, “বের হলো” লেখাগুলো কোন ক্রমে আসে।
// শেখার মডেল: Express নয়, ধারণা দেখানোর জন্য হাতে লেখা ছোট runner
function run(stack, req, res) {
let i = 0;
function next() {
const fn = stack[i++];
if (fn) fn(req, res, next);
}
next();
}
const logger = (req, res, next) => {
console.log("1 logger: ঢুকল", req.url);
next();
console.log("1 logger: বের হলো");
};
const shop = (req, res, next) => {
req.shop = "বাজারঘর";
console.log("2 shop: req.shop বসাল");
next();
console.log("2 shop: বের হলো");
};
const handler = (req, res) => {
res.body = req.shop + " এর পণ্য";
console.log("3 handler: উত্তর তৈরি, next ডাকেনি");
};
const req = { url: "/api/products" };
const res = {};
run([logger, shop, handler], req, res);
console.log("শেষ ফল:", res.body);আসল আউটপুট:
1 logger: ঢুকল /api/products
2 shop: req.shop বসাল
3 handler: উত্তর তৈরি, next ডাকেনি
2 shop: বের হলো
1 logger: বের হলো
শেষ ফল: বাজারঘর এর পণ্য
next() একটি সাধারণ ফাংশন কল। তাই next()-এর পরের লাইন চলে ভেতরের সবার কাজ শেষে, ঠিক উল্টো ক্রমে: 3, 2, 1. আর req.shop পরের ফাংশনে পৌঁছায়, কারণ একই req object সবার হাত ঘুরে যায়।
এই উল্টো ক্রম শুধু synchronous কাজে মেলে। Express-এর next() পরের ফাংশনের await শেষ হওয়ার জন্য অপেক্ষা করে না। সত্যিকারের Express 5.3.0-এ handler 30ms অপেক্ষা করলে ফল এমন হলো:
logger before
logger after next()
handler sends
অর্থাৎ next()-এর ঠিক পরে সময় মাপলে handler-এর সময় বাদ পড়ে। সময় মাপতে তাই res.on("finish", ...) লাগে, যা অংশ ৭-এ লিখব।
৩ · ক্রম আর স্তর — কে কখন, কোথায় চলবে
Express ফাংশনগুলোকে যে ক্রমে লিখেছেন, ঠিক সেই ক্রমে সাজিয়ে রাখে। আলাদা কোনো “priority” নেই। এর ফল কতটা বাস্তব, একটি ছোট সত্যিকারের run-এ দেখুন। প্রথম route express.json()-এর আগে, দ্বিতীয়টি পরে:
// ভুল ক্রম: route আগে, parser পরে
app.post("/wrong", (req, res) => {
res.json({ typeofBody: typeof req.body, body: req.body ?? null });
});
app.use(express.json());
app.post("/right", (req, res) => {
res.json({ typeofBody: typeof req.body, body: req.body });
});সত্যিকারের run, Express 5.3.0, Node 24 (লেখার সময়ের stable version):
$ curl -s -X POST -H 'Content-Type: application/json' -d '{"a":1}' localhost:47328/wrong
{"typeofBody":"undefined","body":null}
$ curl -s -X POST -H 'Content-Type: application/json' -d '{"a":1}' localhost:47328/right
{"typeofBody":"object","body":{"a":1}}
একই request, একই body, শুধু লাইনের জায়গা আলাদা। Error নেই, শুধু খালি ফল। Express 5-এ body parse না হলে req.body হয় undefined; Express 4-এ এটি {} ছিল। পুরোনো কোডে req.body.item লিখলে এখন তাই TypeError আসতে পারে।
তিন স্তরে middleware
Middleware কোথায় বসাবেন, তার ওপর নির্ভর করে সে কতগুলো request দেখবে। স্কুলের গেটের দারোয়ান সবাইকে দেখেন, বিজ্ঞান ভবনের পাহারাদার শুধু সেই ভবনের লোকদের, আর ল্যাবের শিক্ষক শুধু ল্যাবে ঢোকা শিক্ষার্থীদের।
| স্তর | কোড | কে দেখে | সাধারণ ব্যবহার |
|---|---|---|---|
| App-level | app.use(timer) | সব request | log, timer, helmet |
| Path সহ app-level | app.use("/api", cors()) | /api দিয়ে শুরু হওয়া সব | cors, json parser |
| Router-level | orders.use(fn) | শুধু এই router-এর request | একটি অংশের নিয়ম |
| Route-level | orders.post("/", requireAuth, create) | শুধু এই একটি route | auth, validation |
Router-level middleware-এর একটি কাজের ব্যবহার হলো একটি অংশকে শর্তসাপেক্ষে পাশ কাটানো। নিচের router x-beta header না পেলে next("router") ডেকে নিজে থেকে বেরিয়ে যায়, তাই app-এর পুরোনো route উত্তর দেয়:
const beta = express.Router();
beta.use((req, res, next) => (req.get("x-beta") ? next() : next("router")));
beta.get("/feed", (req, res) => res.send("নতুন feed\n"));
app.use(beta);
app.get("/feed", (req, res) => res.send("পুরোনো feed\n"));$ curl -s localhost:47333/feed
পুরোনো feed
$ curl -s localhost:47333/feed -H 'x-beta: 1'
নতুন feed
পাশাপাশি আরও দুটি বিষয় খেয়াল করুন। app.use("/api", fn) path-এর শুরু মেলায়, আর app.get("/api/x", fn) পুরো path আর method মেলায়। আর mount করা middleware-এর ভেতরে req.url ছোট হয়ে যায়। সত্যিকারের run-এর log:
A all
B /api, req.url = /x baseUrl = /api
C route 1
D route 2
এখানে GET /api/x গিয়েছিল। Mount path বাদ দিয়ে req.url হয়েছে /x, তাই log-এ পুরো ঠিকানা চাইলে req.originalUrl লাগে। আর “C route 1” next("route") ডেকেছিল, তাই তার সঙ্গী ফাংশনটি বাদ পড়ে পরের route “D” চলেছে।
ক্রমের একটি ভালো ছাঁচ: সাধারণত আগে log ও timer, তারপর helmet, cors, rate limit, body parser, এরপর auth, route, আর একদম শেষে error handler. Cookie বা session-এর মতো খরচের কাজ যেসব route-এ লাগে না, সেখানে চালাবেন না। Static ফাইলের জন্য database-এ session খোঁজা অকারণ খরচ।
৪ · Built-in middleware — json, urlencoded আর static
Express-এর সঙ্গেই কয়েকটি middleware আসে। আলাদা body-parser package আর ইনস্টল করতে হয় না; express.json() ভেতরে সেটিই ব্যবহার করে।
express.json আর body-র সীমা
এটি request-এর body পড়ে JSON parse করে। মনে রাখবেন, Content-Type JSON না হলে সে কিছুই করে না। সত্যিকারের run-এ text/plain পাঠালে handler পেল {"bodyType":"undefined"}.
সীমা কেন? দোকানের কাউন্টারে কেউ ট্রাকভর্তি মাল ঢোকাতে চাইলে দরজাতেই থামানো সহজ। Body পড়তে memory আর CPU লাগে, তাই সীমার বাইরে হলে আগেই থামানো ভালো। ডিফল্ট সীমা 100kb, মানে 100 × 1024 = 102400 byte. বাজারঘরের order API-তে এত বড় JSON লাগে না, তাই পরীক্ষায় সীমা রাখা হলো 1kb (1024 byte), আর পাঠানো হলো 2014 byte:
app.use("/api", express.json({ limit: "1kb" }));$ wc -c big.json
2014 big.json
$ curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:47326/api/orders \
-H 'Authorization: Bearer example-token' \
-H 'Content-Type: application/json' --data-binary @big.json
413
Server log-এ expected: 2014, limit: 1024, type: 'entity.too.large' ছিল। ভাঙা JSON পাঠালে entity.parse.failed সহ 400 আসে। দুটিই error পথে যায়, আর ডিফল্ট error পাতায় development mode-এ stack trace দেখা গেছে। পর্ব ২৮-এ এগুলোকে নিরাপদ JSON বার্তায় বদলাবেন।
express.urlencoded
সাধারণ HTML form ডিফল্টভাবে name=rina&age=20 আকারে ডেটা পাঠায়। এই middleware সেটি পড়ে। Express 5-এ extended ডিফল্ট false, তাই nested নাম ভাঙে না:
$ curl -s -X POST localhost:47326/form/deep --data 'user[name]=rina&a=1&a=2'
{"user[name]":"rina","a":["1","2"]}
Express 4-এর অনেক টিউটোরিয়াল extended: true ধরে নেয়, যেখানে user একটি object হতো। দরকার হলে নিজে থেকে চালু করতে হবে, আর তখন যাচাই আরও জরুরি (পর্ব ২৭)।
express.static
একটি folder-এর ফাইল সরাসরি পরিবেশন করে। Disk-এ ফাইল পেলে উত্তর পাঠিয়ে chain থামায়, না পেলে next() ডাকে। Express 5-এ dotfile ডিফল্টভাবে উপেক্ষিত, তাই লুকানো .env ফাইল থাকলেও পাওয়া যায় না:
$ curl -si localhost:47326/assets/site.css
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Content-Type: text/css; charset=utf-8
Content-Length: 15
…
$ curl -s -o /dev/null -w 'env: %{http_code}\n' localhost:47326/assets/.env
env: 404
তবু এই 404-এর ভরসায় .env static folder-এ রাখবেন না। Cache-Control: public, max-age=0 মানে ডিফল্টে প্রতিবার যাচাই হয়; maxAge option দিয়ে বদলানো যায়, যার হিসাব পর্ব ৮৪-তে।
৫ · CORS — আসলে কে কাকে আটকায়
পর্ব ০২-এ দেখেছেন, scheme, host আর port মিলে হয় একটি origin. বাজারঘরের React অ্যাপ চলে localhost:47327-এ, API চলে localhost:47326-এ। Port আলাদা, তাই origin আলাদা। Browser-এর same-origin policy বলে, একটি origin-এর JavaScript অন্য origin-এর উত্তর পড়তে পারবে না। CORS হলো server-এর দেওয়া একটি অনুমতিপত্র: “এই origin আমার উত্তর পড়তে পারে।”
হোটেলের রিসেপশন ভাবুন। অতিথির হাতে একটি পার্সেল এলো অন্য কারও নামে। রিসেপশনিস্ট (browser) প্রেরকের চিঠি দেখেন: “অমুক অতিথিকে এটি দেওয়া যাবে।” চিঠি না থাকলে পার্সেল অতিথির হাতে যায় না। কিন্তু এই নিয়ম হোটেলের বাইরে চলে না। রাস্তায় কেউ সরাসরি প্রেরকের কাছে গেলে (curl) রিসেপশনিস্ট থাকেনই না। তাই CORS browser-এ থাকা ব্যবহারকারীকে রক্ষা করে, server-কে নয়।
সত্যিকারের cors package
import cors from "cors";
const allowed = ["http://localhost:47327"];
app.use("/api", cors({
origin: allowed,
credentials: true,
methods: ["GET", "POST", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
maxAge: 600,
}));এখন অজানা origin বসিয়ে curl চালাই (cors 2.8.6, সত্যিকারের run; এই পর্বের curl run-গুলোতে ঠিকানা আসলে ছিল 127.0.0.1):
$ curl -si localhost:47326/api/products -H 'Origin: http://evil.example'
HTTP/1.1 200 OK
Vary: Origin
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
…
[{"id":1,"name":"চাল"}]
ডেটা চলে এসেছে! Access-Control-Allow-Origin header শুধু নেই। curl সেটি দেখেই না। কিন্তু Microsoft Edge 156-এ একই কাজ অজানা origin-এর পাতা থেকে করলে console-এ যা এলো:
Access to fetch at 'http://localhost:47326/api/products' from origin
'http://localhost:47329' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.
JS: TypeError: Failed to fetch
আর server log বলছে, ঠিক সেই সময়ে:
handler চলল, products পাঠানো হচ্ছে
GET /api/products 200 0.8ms
এই পর্বের সবচেয়ে গুরুত্বপূর্ণ পর্যবেক্ষণ: Handler চলেছে, উত্তর 200 হয়েছে। Browser শুধু JavaScript-কে উত্তরটি পড়তে দেয়নি। তাই CORS দিয়ে login, permission বা CSRF সুরক্ষার কাজ চলে না। Data-বদলানো কাজের আলাদা সুরক্ষা লাগে, যা আছে CSRF ও cookie পর্বে। Same-origin policy আর ভুল configuration-এর পুরো তত্ত্ব আছে web নিরাপত্তা মডেলে।
Preflight — আসল অনুরোধের আগের প্রশ্ন
সাধারণ GET বা form-এর মতো POST আগে থেকেই ওয়েবে চলে আসছে, তাই browser সেগুলো সরাসরি পাঠায়। কিন্তু Content-Type: application/json, Authorization header, বা DELETE থাকলে browser আগে OPTIONS দিয়ে জিজ্ঞেস করে নেয়। একেই বলে preflight.
- React কোড
fetchPOST চায় - Browser
OPTIONSআগে জিজ্ঞাসা - cors
204অনুমতি - Browser
POSTআসল অনুরোধ - Handler
201উত্তর পড়া গেল
fetch("http://localhost:47326/api/orders", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json",
"Authorization": "Bearer example-token" },
body: JSON.stringify({ item: "chal" }),
});
OPTIONS /api/orders HTTP/1.1
Origin: http://localhost:47327
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization,content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:47327
Vary: Origin
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,DELETE
Access-Control-Allow-Headers: Content-Type,Authorization
Access-Control-Max-Age: 600
POST /api/orders HTTP/1.1
Origin: http://localhost:47327
Authorization: Bearer example-token
Content-Type: application/json
{"item":"chal"}
OPTIONS /api/orders 204 1.3ms
orders router: POST /
POST /api/orders 201 9.5ms
JS: status 201 {"by":7,"item":"chal"}
- JavaScript POST চাইল। এতে JSON
Content-TypeআরAuthorizationআছে, আরcredentials: "include"মানে cookie-ও যাবে। সাধারণ অনুরোধের শর্ত ভেঙেছে। - Browser নিজে OPTIONS পাঠাল। এই অনুরোধ আপনার কোড লেখেনি। এতে credentials থাকে না; শুধু জানায় কোন origin, কোন method, কোন header চাই। এই একটি তথ্য পরে একটি বড় ভুল ব্যাখ্যা করবে।
corsউত্তর দিল। OPTIONS দেখে সে নিজেই 204 পাঠাল, chain এখানেই থামল।express.json, auth বা handler কেউ চলেনি।Access-Control-Max-Age: 600মানে এই অনুমতি ৬০০ সেকেন্ড মনে রাখা যাবে।- Browser মিলিয়ে দেখল, তারপর POST পাঠাল। Origin, method, দুটি header আর credentials, সবই অনুমতিতে আছে। এবার আসল অনুরোধ গেল, এবং এটি পুরো পাইপলাইন পার হবে।
- উত্তর JavaScript-এর হাতে। উত্তরেও
Access-Control-Allow-Originমিলেছে, তাইfetch201 পড়তে পারল। Server log-এ একটি ক্লিকের জন্য দুটি লাইন, এটাই preflight-এর চিহ্ন।
অজানা origin-এর ক্ষেত্রে cors তালিকা মেলাতে না পেরেও 204 দেয়, শুধু Allow-Origin header বাদ রাখে। সিদ্ধান্ত নেয় browser. নিচের ব্লকে সেই ধাপগুলো দেখুন। এটি শেখার মডেল: Node-এর fetch CORS মানে না, তাই browser-এর হিসাব হাতে লেখা।
// শেখার মডেল: node-এর fetch CORS মানে না, তাই browser-এর সিদ্ধান্ত হাতে লেখা
const http = require("node:http");
const ALLOWED = ["https://shop.example"];
let handlerRuns = 0;
const server = http.createServer((req, res) => {
const origin = req.headers.origin;
if (ALLOWED.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Vary", "Origin");
}
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Methods", "GET,POST");
return res.writeHead(204).end();
}
handlerRuns++;
res.end("order saved");
});
async function browserModel(url, origin) {
const pre = await fetch(url, { method: "OPTIONS", headers: { origin,
"access-control-request-method": "POST" } });
const allow = pre.headers.get("access-control-allow-origin");
console.log(origin, "preflight:", pre.status, "allow-origin:", allow);
if (allow !== origin) return console.log(" browser: আসল POST পাঠাল না");
const r = await fetch(url, { method: "POST", headers: { origin } });
console.log(" browser: POST গেল,", r.status, await r.text());
}
server.listen(0, "127.0.0.1", async () => {
const url = `http://127.0.0.1:${server.address().port}/api/orders`;
await browserModel(url, "https://shop.example");
await browserModel(url, "https://other.example");
console.log("handler মোট চলল:", handlerRuns);
server.close();
});আসল আউটপুট:
https://shop.example preflight: 204 allow-origin: https://shop.example
browser: POST গেল, 200 order saved
https://other.example preflight: 204 allow-origin: null
browser: আসল POST পাঠাল না
handler মোট চলল: 1
দুটি preflight-ই 204, পার্থক্য শুধু header-এ। দ্বিতীয় ক্ষেত্রে POST যায়ইনি, তাই handler একবারই চলেছে। এই কারণেই preflight গুরুত্বপূর্ণ: GET-এর মতো “চলে গেল, শুধু পড়া গেল না” না হয়ে অনুরোধটিই থেমে যায়। Edge-এও একই ফল এসেছিল: Response to preflight request doesn't pass access control check.
Credentials আর *-এর নিয়ম
cors({ origin: "*", credentials: true }) লিখলে package আপত্তি করে না, দুটি header-ই পাঠায়। আপত্তি করে browser. credentials: "include" দিয়ে Edge-এ সত্যিকারের ফল:
Access to fetch at 'http://localhost:47326/open/ping' from origin
'http://localhost:47327' has been blocked by CORS policy: The value of the
'Access-Control-Allow-Origin' header in the response must not be the wildcard
'*' when the request's credentials mode is 'include'.
Credentials ছাড়া একই ঠিকানায় status 200 {"ok":true} এসেছে। নিয়মটির কারণ পরিষ্কার: ব্যবহারকারীর cookie-সহ উত্তর “যে কেউ পড়তে পারে”, এমন অনুমতি বিপজ্জনক। তবে এর চেয়েও খারাপ একটি পথ আছে, যা error দেয় না:
// cors({ origin: true, credentials: true })
$ curl -si localhost:47330/reflect/me -H 'Origin: http://evil.example'
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://evil.example
Vary: Origin
Access-Control-Allow-Credentials: true
origin: true যেকোনো origin ফিরিয়ে দেয়। Browser-এর *-নিষেধ পার হয়ে যায়, কিন্তু কার্যত সবাইকে cookie-সহ পড়ার অনুমতি দেয়। সবসময় জানা origin-এর একটি allowlist রাখবেন। আর dynamic origin ফেরত দিলে Vary: Origin থাকা চাই, যাতে একজনের জন্য cache করা উত্তর অন্যজন না পান। cors এটি নিজেই বসায়।
ক্রমের সত্যিকারের ফাঁদ: auth আগে, cors পরে
// ভুল: auth cors-এর আগে
app.use("/bad", requireAuth, cors({ origin: allowed, credentials: true }));Token ঠিক থাকলেও Edge-এ এলো preflight error, আর server log-এ দেখা গেল OPTIONS /bad/orders 401 0.4ms. কারণ ধাপ ২-এই বলেছি: preflight-এ Authorization থাকে না। Auth তাকে 401 দিয়ে থামাল, cors পর্যন্ত যাওয়াই হলো না। সমাধান: cors আগে। একই কারণে rate limiter cors-এর আগে বসালে 429 উত্তরে CORS header থাকে না, আর browser-এ ব্যবহারকারী দেখবেন CORS error, আসল 429 নয়।
CORS error দেখলে কোথায় তাকাবেন
Browser-এর error-এর ভাষা দেখলেই অর্ধেক কাজ হয়ে যায়। DevTools-এর Network ট্যাবে OPTIONS অনুরোধটি খুলে নিচের তালিকা মেলান:
| Console-এ যা লেখা | সম্ভাব্য কারণ | প্রথম যাচাই |
|---|---|---|
Response to preflight request doesn't pass | OPTIONS-এ Allow-Origin নেই; auth বা অন্য কিছু আগে থেমেছে | OPTIONS-এর status আর header |
No 'Access-Control-Allow-Origin' header | Origin তালিকায় নেই, বা error উত্তর cors-এর আগে গেছে | তালিকার বানান: scheme, host, port |
must not be the wildcard '*' | Credentials-এর সঙ্গে * | নির্দিষ্ট origin বসানো |
একটি ছোট বিষয়ও বহু সময় নষ্ট করে: http://localhost:5173 আর http://127.0.0.1:5173 আলাদা origin, আর শেষে / লেখা origin-ও তালিকায় মিলবে না। উত্তরের কোনো custom header JavaScript-কে পড়াতে চাইলে exposedHeaders option লাগে; ডিফল্টে অল্প কিছু header-ই পড়া যায়।
Preflight-এর খরচ — একটি হিসাব
ধরে নিই browser থেকে server-এর round trip 80 ms, আর একটি session-এ ৩টি আলাদা URL-এ মোট ৩০টি preflight-দরকারি অনুরোধ যায়। Preflight cache-এর key-তে URL থাকে, তাই প্রতিটি URL-এর জন্য আলাদা হিসাব।
- Cache কাজে না লাগলে: ৩০ × 80 ms = 2400 ms বাড়তি অপেক্ষা (মোট যোগফল)।
- ৬০০ সেকেন্ড cache থাকলে এবং সব অনুরোধ সেই সময়ের মধ্যে হলে: ৩ × 80 ms = 240 ms.
- সাশ্রয়: 2400 − 240 = 2160 ms, অর্থাৎ ৯০%.
এখানে 80 ms আর ৩০টি অনুরোধ অনুমান, মাপা ফল নয়। /api/orders/7 আর /api/orders/8 আলাদা URL, তাই পথে id থাকলে cache কম কাজে লাগে। Frontend আর API একই origin-এ রাখলে (যেমন reverse proxy দিয়ে) preflight-ই লাগে না।
৬ · helmet, compression আর rate limit — তিন দরকারি প্রতিবেশী
এগুলোর তত্ত্ব অন্য কোর্সে আছে। এখানে শুধু দেখব middleware হিসেবে কোথায় বসে, আর সত্যিকারের ফল কী।
helmet — security header
helmet নিজে কিছু আটকায় না; সে উত্তরে কিছু header বসায়, যা browser মানে। Helmet 8.3.0-এর ডিফল্টে আসা কয়েকটি header:
$ curl -si localhost:47326/safe/ping
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;…
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 0
…
প্রতিটি header-এর অর্থ আর ধাপে ধাপে চালুর পথ আছে Web hardening পর্বে। CSP ভিত্তি আছে XSS ও CSP-তে। HTML পরিবেশন করা অ্যাপে ডিফল্ট CSP বাইরের script বা font আটকাতে পারে, তাই চালুর পর পাতা ঘুরে দেখা দরকার।
compression — উত্তর ছোট করা
৫০০টি পণ্যের JSON-এ সত্যিকারের ফল, compression 1.8.2:
plain 29393 bytes
gzip 1368 bytes
Vary: Accept-Encoding
Content-Encoding: gzip
হিসাব: 1368 ÷ 29393 = ০.০৪৬৫, অর্থাৎ আকার প্রায় ৪.৭%, কমেছে প্রায় ৯৫.৩%. এই অনুপাত ফোলানো, কারণ ৫০০টি সারি প্রায় একই লেখা। আসল ডেটায় কম হবে। gzip-এ CPU লাগে, তাই সামনে nginx বা CDN থাকলে অনেকে কাজটি সেখানেই করেন। ছোট উত্তর ডিফল্টে compress হয় না; সীমা 1kb.
express-rate-limit — গতি বাঁধা
একই ঠিকানা থেকে অনেকবার login চেষ্টা থামাতে rate limit লাগে। Express-এর চোখে এটি আর দশটি route-level middleware-এর মতোই:
import { rateLimit } from "express-rate-limit";
const loginLimiter = rateLimit({
windowMs: 60_000, limit: 3,
standardHeaders: "draft-8", legacyHeaders: false,
message: { error: "অনেকবার চেষ্টা হয়েছে" },
});
app.post("/api/login", loginLimiter, loginHandler);# একই ঠিকানায় চারবার POST (express-rate-limit 8.7.2)
HTTP/1.1 401 Unauthorized
RateLimit: "3-in-1min"; r=2; t=60
HTTP/1.1 401 Unauthorized
RateLimit: "3-in-1min"; r=1; t=60
HTTP/1.1 401 Unauthorized
RateLimit: "3-in-1min"; r=0; t=60
HTTP/1.1 429 Too Many Requests
RateLimit: "3-in-1min"; r=0; t=60
Retry-After: 60
r হলো বাকি চেষ্টা। চতুর্থবার handler চলেইনি। দুটি বাস্তব সতর্কতা:
- Proxy-র পেছনে: Client-এর IP না বুঝলে সবাই একই ঠিকানা মনে হয়।
X-Forwarded-Forএসেছে অথচ trust proxy সেট নেই দেখে package-টি সত্যিকারের run-এERR_ERL_UNEXPECTED_X_FORWARDED_FORlog করেছিল। Proxy-র সংখ্যা জেনে সঠিক মান দিতে হয়; অন্ধভাবেtrueনয়। - একাধিক process: ডিফল্ট store প্রতিটি process-এর memory-তে থাকে। ৪টি process-এ সীমা ৪ গুণ হয়ে যায়, তাই ভাগ করা store লাগে (Redis, পর্ব ৪৪)।
কোথায় কত সীমা, quota কী, gateway-র ভূমিকা কী, এগুলো আছে API security আর DDoS প্রতিরক্ষা পর্বে। Login-এর পুরো নিয়ম পর্ব ৪৯-এ।
৭ · নিজের middleware — timer, logger আর auth
নিজের middleware লেখা মানে একটি ফাংশন লেখা। বাজারঘরের তিনটি সত্যিকারের নমুনা, যেগুলো এই পর্বের সব run-এ চলেছে:
import { performance } from "node:perf_hooks";
// ১. Timer + logger: উত্তর শেষে একটি লাইন লেখে
function requestTimer(req, res, next) {
const start = performance.now();
res.on("finish", () => {
const ms = (performance.now() - start).toFixed(1);
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${ms}ms`);
});
next();
}
// ২. Router-এর নিজের logger
orders.use((req, res, next) => {
console.log("orders router:", req.method, req.url);
next();
});
// ৩. সহজ auth check, শেখার জন্য
function requireAuth(req, res, next) {
const header = req.get("authorization");
if (header !== "Bearer example-token") {
return res.status(401).json({ error: "লগইন দরকার" });
}
req.user = { id: 7, role: "seller" };
next();
}
orders.post("/", requireAuth, createOrder);একটি নির্দিষ্ট token কোডে লেখা শুধু এখানে ধারণা দেখানোর জন্য। আসল login-এ session বা JWT যাচাই হবে (পর্ব ৪৫ ও ৪৬), আর secret থাকবে environment-এ (পর্ব ৭৮)। Logger-এ Authorization, cookie বা password লিখবেন না। return শব্দটিও বাদ দেবেন না, নইলে কোড নিচে গিয়ে next()-ও ডাকবে।
এবার ধাপে ধাপে দেখুন, token ছাড়া একটি অনুরোধে কী হয়।
1app.use(requestTimer);2app.use("/api", cors(options));3app.use("/api", express.json());4orders.post("/", requireAuth, createOrder);56function requireAuth(req, res, next) {7 if (header !== "Bearer example-token")8 return res.status(401).json({ ... });9 req.user = { id: 7, role: "seller" };10 next();11}
| header | |
| statusCode | |
| req.user | |
| createOrder |
- requestTimer চলল। সময় নিল,
finish-এর কাজ জমা রাখল,next()ডাকল। - cors চলল। Header ঠিক করল। এটি preflight নয়, তাই
next(). - express.json চলল। Body পড়ল। Body-তে সমস্যা থাকলে chain এখানেও থামত।
- Route মিলল। Route-এর প্রথম ফাংশন
requireAuthশুরু হলো।createOrderএখনো তার পেছনে অপেক্ষায়। - Header যাচাই।
req.get("authorization")ফেরত দিলundefined, যা token-এর সঙ্গে মেলে না। তাইif-এর ভেতরে ঢুকল। - 401, chain থামল। উত্তর পাঠানো হলো,
returnফাংশন শেষ করল।next()ডাকা হয়নি, তাইcreateOrderবাদ।req.userবসেওনি। - Timer-এর log. Chain থামলেও
finishচলে, তাই ব্যর্থ অনুরোধও log-এ আছে। 9.7ms একবারের স্থানীয় মাপ। Database-এ একটিও লেখা হয়নি, ভুল অনুরোধ সস্তায় থেমেছে।
হাতে লেখা chain-এ একই আচরণ চালিয়ে দেখুন। এটিও শেখার মডেল, কিন্তু HTTP সত্যিকারের: node:http server নিজের সঙ্গে কথা বলছে।
// শেখার মডেল: node:http-এর ওপর হাতে লেখা chain, Express নয়
const http = require("node:http");
function timer(req, res, next) {
const start = performance.now();
res.on("finish", () => {
const ms = performance.now() - start;
console.log("timer:", req.method, req.url, res.statusCode, ms >= 20 ? ">=20ms" : "<20ms");
});
next();
}
function auth(req, res, next) {
if (req.headers.authorization !== "Bearer example-token") {
res.statusCode = 401;
return res.end("login দরকার"); // chain এখানেই শেষ
}
req.user = { id: 7 };
next();
}
function orders(req, res) {
setTimeout(() => res.end("user " + req.user.id + " এর order"), 25); // database-এর ভান
}
const stack = [timer, auth, orders];
const server = http.createServer((req, res) => {
let i = 0;
(function next() { stack[i++]?.(req, res, next); })();
});
server.listen(0, "127.0.0.1", async () => {
const url = `http://127.0.0.1:${server.address().port}/orders`;
const a = await fetch(url);
console.log("token ছাড়া:", a.status, await a.text());
const b = await fetch(url, { headers: { authorization: "Bearer example-token" } });
console.log("token সহ:", b.status, await b.text());
server.close();
});আসল আউটপুট:
timer: GET /orders 401 <20ms
token ছাড়া: 401 login দরকার
timer: GET /orders 200 >=20ms
token সহ: 200 user 7 এর order
Auth থামিয়ে দিলে 25ms-এর database ধাপ চলেইনি, তাই প্রথম অনুরোধ দ্রুত। মিলিসেকেন্ড মেশিনভেদে বদলায়, তাই আউটপুটে শুধু তুলনা রাখা হয়েছে। next() আগেই ফিরে গেলেও timer সঠিক সময় পেয়েছে, কারণ সে finish event শোনে।
Route-level, না সব জায়গায়? পুরো router-এ orders.use(requireAuth) বসালে একটি নতুন route-এ auth ভুলে যাওয়ার ঝুঁকি কমে। Route-level-এ public আর private route মেশানো সহজ। সব route-কে শুরুতে private ধরে public-গুলো আলাদা তালিকায় রাখা সাধারণত নিরাপদ। ভূমিকা-ভিত্তিক permission আসছে পর্ব ৪৮-এ, তত্ত্ব আছে Broken Access Control-এ।
Option নেওয়া middleware — ফাংশন যে ফাংশন বানায়
express.json({ limit }) বা cors({...}) নিজে middleware নয়; তারা ডাকলে একটি middleware ফেরত দেয়। দর্জির কথা ভাবুন: মাপ নিয়ে তিনি একটি জামা বানান, আর সেই জামা ওই মাপেরই। নিজের কোডেও একই ধাঁচ:
function requireRole(role) {
return function (req, res, next) {
if (req.user?.role !== role) {
return res.status(403).json({ error: "অনুমতি নেই" });
}
next();
};
}
app.delete("/products/:id", requireAuth, requireRole("admin"), removeProduct);# সত্যিকারের run; এখানে req.user একটি demo middleware বসিয়েছিল
$ curl -s -w ' [%{http_code}]\n' -X DELETE localhost:47333/products/5 # role: seller
{"error":"অনুমতি নেই"} [403]
$ curl -s -w ' [%{http_code}]\n' -X DELETE localhost:47333/products/5 # role: admin
[204]
এখানে role মনে থাকে closure-এর কারণে। 401 মানে “আপনাকে চিনি না”, 403 মানে “চিনি, কিন্তু অনুমতি নেই”; এই দুটির বিস্তার পর্ব ৩৩-এ। ভুলে app.use(requireRole) লিখবেন না: Express তখন ফাংশন-বানানো ফাংশনটিকেই middleware ধরে ডাকবে, সে শুধু একটি ফাংশন ফেরত দেবে, আর request ঝুলে থাকবে।
TypeScript-এ req.user: Express-এর type-এ user নেই, তাই req.user লিখলে compiler আপত্তি করে। সাধারণ সমাধান declaration merging দিয়ে Express.Request-এ field যোগ করা। তবে এটি শুধু compiler-কে বলা “এটি আছে”; ক্রম ভুল হলে runtime-এ মানটি থাকবে না। Type-এর ভিত্তি পর্ব ০৬-এ দেখেছেন। একই request-এর ভেতরে ডেটা রাখতে res.locals-ও ব্যবহার করা যায়।
৮ · ঝুলে থাকা request, দুইবার উত্তর আর async middleware
ভুলে যাওয়া next
function forgotNext(req, res, next) {
console.log("forgotNext চলল, কিন্তু next ডাকেনি");
}
app.get("/stuck", forgotNext, (req, res) => res.json({ ok: true }));$ curl -s --max-time 2 localhost:47326/stuck; echo "curl exit: $?"
curl exit: 28
# server log
forgotNext চলল, কিন্তু next ডাকেনি
কোনো error নেই, কোনো status নেই। Exit 28 মানে curl-এর নিজের timeout শেষ হয়েছে। Server log-এ timer-এর লাইনও নেই, কারণ উত্তর শেষ না হলে finish কখনো চলে না। ব্যবহারকারী শুধু ঘুরতে থাকা চিহ্ন দেখেন। এই ধরনের অনুরোধ connection আটকে রাখে, তাই জমতে থাকলে server-ও ভোগে।
সেজন্য টাইমারে close event-ও শোনা ভালো। সত্যিকারের পরীক্ষায় client সরে যেতেই close fired, writableFinished = false এসেছে, আর finish আসেনি। writableFinished দেখে অসম্পূর্ণ অনুরোধ আলাদা করা যায়। পর্ব ৮৩-এ এমন অনুরোধ monitoring-এ ধরবেন।
উল্টো ভুল: দুইবার উত্তর
উত্তর পাঠিয়ে আবার next() ডাকলে পরের ফাংশনও উত্তর পাঠাতে চায়। Client প্রথম উত্তর {"a":1} পেয়েছিল, কিন্তু server log-এ এলো:
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (node:_http_outgoing:642:11)
…
নিয়মটি সহজ: একটি request-এ উত্তর একবার। Middleware হয় উত্তর দেবে, নয় next() ডাকবে; সাধারণত দুটি একসঙ্গে নয়। তাই return res... লেখা ভালো অভ্যাস।
Express 5-এ async middleware
Express 4-এ async ফাংশনের ভেতরে throw করলে Express জানতেই পারত না, তাই প্রতিটি জায়গায় try/catch বা wrapper লাগত। Express 5-এ middleware বা handler একটি rejected promise ফেরত দিলে তা নিজে থেকেই next(err)-এ যায়।
async function loadShop(req, res, next) {
await new Promise((r) => setTimeout(r, 5));
if (req.query.shop === "bad") throw new Error("shop পাওয়া যায়নি");
req.shop = { id: 1 };
next();
}
app.get("/shop", loadShop, (req, res) => res.json(req.shop));$ curl -s -w ' [%{http_code}]\n' localhost:47326/shop
{"id":1} [200]
$ curl -s -w ' [%{http_code}]\n' 'localhost:47326/shop?shop=bad'
<!DOCTYPE html> … <pre>Error: shop পাওয়া যায়নি … </pre> … [500]
Process পড়ে যায়নি, request ঝোলেনি, 500 এসেছে। Stack trace পাতায় দেখা যাচ্ছে কারণ এটি development; এই ডিফল্ট পাতা ব্যবহারকারীকে দেখানোর জন্য নয়। নিজের error middleware (err, req, res, next) আর নিরাপদ বার্তা আসছে পর্ব ২৮-এ। ভেতরের কৌশলটি মাত্র এক লাইনের। শেখার মডেলে দেখুন:
// শেখার মডেল: Express 5 ভেতরে যেভাবে rejected promise ধরে, তার ছোট রূপ
const http = require("node:http");
const routes = {
"/ok": [async (req, res, next) => { req.shop = 1; next(); },
(req, res) => res.end("shop " + req.shop)],
"/boom": [async () => { throw new Error("shop পাওয়া যায়নি"); }],
"/stuck": [(req, res, next) => { /* next ডাকতে ভুলে গেছে */ }],
};
const server = http.createServer((req, res) => {
const stack = routes[req.url];
let i = 0;
const next = (err) => {
if (err) {
console.log(" server log:", err.message);
res.statusCode = 500;
return res.end("সার্ভারে সমস্যা");
}
const r = stack[i++]?.(req, res, next);
if (r instanceof Promise) r.catch(next); // এই লাইনই আসল জিনিস
};
next();
});
server.listen(0, "127.0.0.1", async () => {
const base = `http://127.0.0.1:${server.address().port}`;
for (const path of ["/ok", "/boom", "/stuck"]) {
try {
const r = await fetch(base + path, { signal: AbortSignal.timeout(400) });
console.log(path, "->", r.status, await r.text());
} catch (e) {
console.log(path, "-> উত্তর এলো না:", e.name);
}
}
server.close();
});আসল আউটপুট:
/ok -> 200 shop 1
server log: shop পাওয়া যায়নি
/boom -> 500 সার্ভারে সমস্যা
/stuck -> উত্তর এলো না: TimeoutError
r.catch(next) লাইনটি rejected promise-কে error পথে পাঠায়। /stuck-এ error-ই হয়নি, তাই Express 5-ও বাঁচাতে পারে না। Client-এর 400 ms timeout শেষে হাল ছেড়েছে। মনে রাখবেন, ধরা পড়ে শুধু ফেরত দেওয়া promise. setTimeout-এর callback-এর ভেতরের throw এই পথে আসে না।
আরও গভীরে: Express-এর router প্রতিটি layer-এর ফাংশন ডেকে ফেরত মান দেখে; সেটি promise হলে তার rejection-কে next-এ পাঠায়। Koa-র মডেল আলাদা: সেখানে await next() সত্যিই ভেতরের সবার শেষ হওয়া পর্যন্ত অপেক্ষা করে, তাই “onion” ধাঁচে সময় মাপা সহজ। HTTP-র দিক থেকে 413 এখন RFC 9110-তে “Content Too Large” নামে আছে, আর 429 এসেছে RFC 6585 (২০১২) থেকে। CORS-এর আজকের নিয়ম আছে WHATWG Fetch Standard-এ।
পরীক্ষা ও ইন্টারভিউতে: Middleware কী? Request আর handler-এর মাঝের ফাংশন, যে req, res বদলাতে, উত্তর দিতে বা next() ডাকতে পারে। next না ডাকলে? উত্তরও না দিলে request ঝুলে থাকে। CORS কে প্রয়োগ করে? Browser; server শুধু header দেয়। Preflight কখন হয়? Simple request-এর শর্ত ভাঙলে, যেমন JSON Content-Type বা Authorization থাকলে। Express 5-এ async error? Rejected promise নিজে থেকেই error middleware-এ যায়।
৯ · AI-এর যুগে middleware
AI middleware লিখতে খুব পটু, কিন্তু তার শেখা ডেটার বড় অংশ Express 4-এর। আর CORS error দেখলে সে প্রায়ই সবচেয়ে চওড়া অনুমতি দিয়ে “সমাধান” করে। এই পর্বে নিচের প্রতিটি সমস্যা সত্যিই চালিয়ে দেখা হয়েছে বা Express 5-এর migration guide-এ মিলিয়ে নেওয়া হয়েছে।
| AI-এর লেখা | সমস্যা | ঠিক পথ |
|---|---|---|
app.options("*", cors()) | Express 5-এ চালু হওয়ার সময়ই ভাঙে: TypeError: Missing parameter name at index 1: * | Router-এর সামনে app.use(cors(...)) |
cors({ origin: true, credentials: true }) | Error ছাড়াই সব origin-কে cookie-সহ অনুমতি | জানা origin-এর তালিকা |
আলাদা body-parser install | অকারণ dependency | express.json() |
asyncHandler wrapper সব জায়গায় | Express 5-এ দরকার নেই | সরাসরি async ফাংশন |
req.body.item সরাসরি পড়া | Parse না হলে TypeError | Parser ক্রম ঠিক রাখা ও যাচাই (পর্ব ২৭) |
try/catch-এর ভেতরে চুপচাপ next() | Error লুকিয়ে পরের ধাপে চলে যায় | next(err), অথবা শুধু throw |
CORS বা middleware-এর সমস্যায় AI-কে পুরো ছবি দিন, আর চওড়া সমাধান আগেই নিষেধ করুন:
AI-এর middleware হাতে পেলে যাচাইয়ের তালিকা
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Middleware ধারণাটি এলো কোথা থেকে, আর প্রায় সব web framework একই পথ নিল কেন?
ধারণাটি Express-এর চেয়ে পুরোনো। Java-র Servlet জগতে ২০০০-এর দশকের শুরুতেই “filter” ছিল, যা request-এর সামনে বসে কাজ করত। Python-এ ২০০৩ সালে PEP 333 (WSGI) একটি সাধারণ চুক্তি দেয়, যাতে একটি app-কে আরেকটি app মুড়ে রাখতে পারে। Ruby-তে ২০০৭ সালে Christian Neukirchen-এর Rack একই চিন্তা আরও জনপ্রিয় করে। Node-এ ২০১০ সালে Connect আসে, আর তার ওপর TJ Holowaychuk-এর Express. (req, res, next) আকারটি Connect থেকেই বড় হয়।
সবাই একই পথে এলো, কারণ সমস্যাটি একই। Log, session, compression, auth, এই কাজগুলো কোনো একটি route-এর নয়, সব route-এর। এদের বলা হয় cross-cutting concern. Handler-এর ভেতরে লিখলে একই কোড শতবার ফিরে আসে। সারিতে সাজালে একবার লিখলেই চলে, আর ছোট টুকরো আলাদা করে test করা যায়।
দ্বিতীয় লাভ বাস্তুতন্ত্র। চুক্তিটি এত সহজ যে যে কেউ একটি npm package লিখতে পারে, যা যেকোনো Express অ্যাপে বসে যায়। এই কারণেই cors, helmet, compression আলাদা package, Express নিজে ছোট থাকে।
দামও আছে। Middleware req-এ যা বসায়, তার কোনো ঘোষণা নেই। req.user কে বসাল, ক্রম ভুল হলে আছে কি না, তা কোড দেখে বোঝা কঠিন। অদৃশ্য নির্ভরতার এই সমস্যা কমাতে অনেক নতুন framework ধরন-নিরাপদ context বা স্পষ্ট hook ব্যবহার করে। Next.js-এর middleware (পর্ব ৫৭) নামে এক হলেও সীমা আলাদা। আবার সারি লম্বা হলে প্রতিটি request-এ প্রতিটি ফাংশনের খরচ জমে, তাই “সব জায়গায় একটু করে” বসানোর আগে হিসাব করা দরকার।
মূল উপলব্ধি: Pipeline হলো দায়িত্ব ভাগের একটি নকশা। শক্তি তার সরলতায়, আর ঝুঁকি লুকানো ক্রমে। তাই app.use-এর তালিকা পড়াকে আর্কিটেকচার পড়ার মতোই গুরুত্ব দিন।
প্র ০২CORS কেন “ঢিলা করার” ব্যবস্থা, আর preflight নামের বাড়তি ধাপ কেন লাগল?
অনেকে ভাবেন CORS একটি দেয়াল। আসলে দেয়ালটি same-origin policy, যা ১৯৯০-এর দশকের মাঝামাঝি Netscape-এর সময় থেকে browser-এ আছে। ধারণাটি ছিল: আপনি যে সাইটে login করা, তার উত্তর অন্য সাইটের script পড়তে পারবে না। পরে API-নির্ভর ওয়েব এলো, আর আলাদা origin-এর API পড়া দরকার হলো। আগে JSONP-এর মতো কৌশল চলত, যা নিরাপদ ছিল না। CORS সেই দেয়ালে একটি নিয়ন্ত্রিত দরজা বানায়, W3C Recommendation হয় ২০১৪ সালে, পরে নিয়ম চলে যায় WHATWG Fetch Standard-এ।
Preflight এসেছে পুরোনো server-দের রক্ষা করতে। CORS-এর আগে কোনো অন্য-origin পাতা browser থেকে সহজে DELETE বা custom header পাঠাতে পারত না। অনেক server তাই ধরে নিয়েছিল, এমন অনুরোধ শুধু নিজের কোড থেকে আসবে। নতুন ব্যবস্থায় হঠাৎ এগুলো যেতে দিলে সেই ধারণা ভেঙে যেত। তাই নিয়ম হলো: নতুন ধরনের অনুরোধ পাঠানোর আগে জিজ্ঞেস করতে হবে। যে server CORS জানে না, সে অনুমতি দেবে না, তাই সে আগের মতোই নিরাপদ থাকে।
অন্যদিকে simple request আগেই form দিয়ে পাঠানো যেত, তাই সেখানে নতুন ঝুঁকি নেই, preflight-ও নেই। এই কারণেই এই পর্বের GET-এ handler চলেছিল। এজন্যই CORS CSRF ঠেকায় না, আর state-বদলানো কাজ GET-এ রাখা উচিত নয়।
এখানে একটি দৈনন্দিন ফলও আছে। Vite-এর dev proxy বা একই origin-এ frontend আর API রাখলে CORS error উধাও হয়, কারণ browser-এর চোখে আর cross-origin অনুরোধ নেই। এটি নিরাপত্তা বাড়ায়ও না, কমায়ও না; শুধু দরকারটি সরিয়ে দেয়।
মূল উপলব্ধি: CORS কোনো আক্রমণ-প্রতিরোধক নয়, এটি পুরোনো দেয়ালের একটি সাবধানী ফাঁক। Server-এ যা সত্যিই আটকাতে চান, তা auth আর authorization দিয়েই আটকাতে হয়।
প্র ০৩Rate limit app-এর ভেতরে রাখবেন, না nginx বা gateway-তে?
দুই জায়গার কাজ আলাদা, আর বড় ব্যবস্থায় সাধারণত দুটিই থাকে। HTTP-তে 429 Too Many Requests এসেছে RFC 6585 (২০১২) থেকে, কিন্তু কোথায় গুনবেন তা কোনো মান বলে দেয় না।
বাইরের স্তর (CDN, reverse proxy, gateway): অনুরোধ Node পর্যন্ত আসার আগেই থামে। তাই app-এর CPU বাঁচে, আর ব্যাপক ভিড় সামলানো যায়। কিন্তু এই স্তর ব্যবসার অর্থ জানে না। “এই seller আজ ৫০টি পণ্য আপলোড করতে পারবেন”, এমন নিয়ম এখানে কঠিন।
App middleware: req.user, plan আর route জানে। Login, OTP, password reset-এর মতো দামি কাজে সূক্ষ্ম নিয়ম বসানো যায়। কিন্তু অনুরোধ ততক্ষণে Node-এ পৌঁছে গেছে। আর ডিফল্ট memory store একাধিক process-এ ভাগ হয় না, তাই সীমা গুণিত হয়।
একটি হিসাব দেখুন। সীমা ১৫ মিনিটে ৫টি login চেষ্টা। দিনে ৯৬টি window, তাই এক IP দিনে ৯৬ × ৫ = ৪৮০ বার চেষ্টা করতে পারে। ৪টি process-এ memory store হলে দিনে ৪৮০ × ৪ = ১৯২০। Fixed window-এ সীমান্তের দুই পাশে অল্প সময়ে ৫ + ৫ = ১০টি অনুরোধও সম্ভব। আবার শুধু IP দিয়ে গুনলে একই NAT-এর পেছনের অনেক ব্যবহারকারী একসঙ্গে আটকে যেতে পারেন, যা বাংলাদেশের মোবাইল নেটওয়ার্কে সহজেই ভাবা যায়। তাই IP-র সঙ্গে account-ভিত্তিক সীমাও লাগে। Token bucket বা sliding window-এর মতো বিকল্প আছে; তুলনা পাবেন API security-তে।
আরেকটি বিবেচনা হলো ব্যর্থতার আচরণ। ভাগ করা store (যেমন Redis) বন্ধ হলে limiter কি সবাইকে আটকাবে, না সবাইকে যেতে দেবে? Login-এর জন্য এক উত্তর, পণ্য-তালিকার জন্য আরেক উত্তর হতে পারে। এটি আগেই ঠিক করে রাখুন।
মূল উপলব্ধি: বাইরের স্তর রক্ষা করে যন্ত্রকে, ভেতরের স্তর রক্ষা করে নিয়মকে। সীমা দেওয়ার আগে হিসাব করুন, কে কতবার পার পাবে, আর কোন নিরীহ ব্যবহারকারী আটকে যাবেন।
প্র ০৪Express-এর next() আর Koa-র await next()-এর পার্থক্য কেন গুরুত্বপূর্ণ?
Express জন্মেছে callback-এর যুগে, ২০১০ সালে। তখন JavaScript-এ async/await ছিল না। next() তাই শুধু পরের ফাংশন ডাকে; সে কিছু ফেরত দেয় না, শেষ হওয়ার খবরও দেয় না। পরের ধাপ অপেক্ষা করলে আগের ধাপ জানতেই পারে না। এই পর্বের সত্যিকারের run-এই “logger after next()” লাইনটি “handler sends”-এর আগে ছাপা হয়েছিল।
Express-এর দলের লোকেরাই ২০১৩ সালের দিকে Koa শুরু করেন, যেখানে middleware পরের ধাপের শেষ পর্যন্ত অপেক্ষা করতে পারে। একে onion model বলা হয়: অনুরোধ স্তর ভেদ করে ভেতরে যায়, তারপর একই স্তর দিয়ে ফেরে। ফলে timer লেখা হয় তিন লাইনে, আর ভেতরের error try/catch দিয়ে এক জায়গায় ধরা যায়।
Express তাহলে বদলাল না কেন? কারণ পিছনের সামঞ্জস্য। হাজার হাজার middleware পুরোনো চুক্তি মেনে লেখা। ২০২৪ সালে এলো Express 5, যাতে rejected promise সামলানো যোগ হলো, কিন্তু next()-এর মৌলিক অর্থ একই রইল। এটি একটি সচেতন আপস: বিশাল বাস্তুতন্ত্র রক্ষা, নতুন সুবিধা ধীরে।
বাস্তবে এর ফল তিনটি। প্রথমত, Express-এ উত্তরের শেষ জানতে event শোনা ছাড়া পথ নেই। দ্বিতীয়ত, উত্তর পাঠানোর পরে পরিবর্তন করার কৌশল (যেমন compression) ভেতরে res-এর ফাংশন মুড়ে কাজ করে, যা জটিল। তৃতীয়ত, Fastify বা Hono-র মতো নতুন framework নিজের hook বা context মডেল বেছে নিয়েছে। কোনটি ভালো তা নিয়ে মতভেদ আছে: Express-এর পক্ষে অসংখ্য package আর পরিচিতি, নতুনগুলোর পক্ষে ধরন-নিরাপত্তা ও পরিষ্কার মডেল।
মূল উপলব্ধি: একটি API-র আকার তার জন্মের সময়ের ভাষার সীমা বহন করে। কোনো অদ্ভুত আচরণ দেখলে ইতিহাস জানা থাকলে ভুলটিও অনুমান করা সহজ হয়।
প্র ০৫একটি route-এ requireAuth ভুলে গেলে কী হয়, আর নকশায় এই ভুল কীভাবে ঠেকাবেন?
ধরুন বাজারঘরে ৮০টি route, আর প্রতিটিতে হাতে requireAuth লেখা। একজন নতুন route যোগ করলেন, GET /api/admin/export, আর লিখতে ভুলে গেলেন। কোনো compile error নেই, কোনো warning নেই। Test না থাকলে route দিব্যি কাজ করবে, সবার জন্য। OWASP-এর তালিকায় ২০২১ সালের সংস্করণে Broken Access Control শীর্ষে উঠে এসেছিল, আর এই ধরনের অনুপস্থিত যাচাই তার একটি পরিচিত রূপ।
এখানে একটি সাধারণ নকশার নীতি কাজে লাগে: deny by default. একা একা মনে রাখার ওপর নির্ভর না করে কাঠামোয় নিরাপত্তা বসান।
- Router ভাগ:
publicRouterআরprivateRouterআলাদা রাখুন, আরprivateRouter.use(requireAuth)লিখুন। নতুন route এখানে গেলে auth নিজে থেকেই আসে। - ক্রম পরীক্ষা:
app.use(publicRouter)আগে, private পরে। ক্রম ভুল হলে উল্টো ক্ষতি হতে পারে, তাই test লাগে। - স্বয়ংক্রিয় test: Route-এর তালিকা ধরে token ছাড়া অনুরোধ পাঠিয়ে 401 নিশ্চিত করুন। পর্ব ৭৫-এ Supertest দিয়ে করবেন।
- Permission আলাদা: Authentication প্রশ্ন করে “আপনি কে”, authorization প্রশ্ন করে “এটি করতে পারেন কি”। Login করা ক্রেতা অন্যের order দেখতে পারবেন না, তাই object-স্তরের যাচাই service-এ লাগে (পর্ব ৪৮)।
খরচের দিকও দেখতে হয়। সব জায়গায় auth বসালে public পণ্য-তালিকাতেও session খোঁজা হবে, তাই “কী public” তা সচেতনভাবে ঠিক করা দরকার। সে তালিকাটি ছোট আর code review-তে সহজে পড়ার মতো রাখলে নতুন route যোগ করার সময় প্রশ্নটি নিজে থেকেই ওঠে। AI দিয়ে route বানালে এই কাঠামো আরও জরুরি, কারণ সে আশেপাশের কোডের ধাঁচ দেখে লেখে, আপনার মনের নিয়ম জানে না।
মূল উপলব্ধি: মানুষ ভুলবেই। ভালো নকশা এমন, যেখানে ভুল করলে দরজা বন্ধ থাকে, খোলা নয়।
অনুশীলন
আগে উত্তর লিখে নিন, তারপর মিলিয়ে দেখুন।
-
Middleware-এর হিসাব: প্রতি request-এ ধরে নিন helmet 0.1 ms, cors 0.1 ms, json 0.8 ms, session খোঁজা 12 ms, rate limit 3 ms, handler 40 ms. (ক) মোট কত, আর middleware-এর অংশ কত শতাংশ? (খ) Session cache করে 1 ms-এ আনলে নতুন মোট কত? (গ) সেকেন্ডে 200টি public request-ও অকারণে 12 ms-এর session খোঁজে। গড়ে একই সময়ে কতটি database কাজ চলছে?
(ক) Middleware: 0.1 + 0.1 + 0.8 + 12 + 3 = ১৬ ms. মোট: 16 + 40 = ৫৬ ms. অংশ: 16 ÷ 56 = ২৮.৬%.
(খ) Middleware হয় 16 − 12 + 1 = ৫ ms, মোট 5 + 40 = ৪৫ ms.
(গ) গড় একসঙ্গে চলা কাজ = হার × সময় = 200 × 0.012 s = ২.৪টি। মানে শুধু অকারণ session-এর জন্য গড়ে ২.৪টি database connection ব্যস্ত। Session middleware-কে private router-এ সরালে এই খরচ চলে যায়। সংখ্যাগুলো অনুমান, নিজের অ্যাপে মেপে বসাবেন।
-
আউটপুট বলুন: Express 5-এ নিচের কোডে
GET /api/xআরGET /homeগেলে console-এ কী ছাপা হবে, আর status কত?app.use((req, res, next) => { console.log("A"); next(); console.log("a"); }); app.use("/api", (req, res, next) => { console.log("B"); next(); }); app.get("/api/x", (req, res) => { console.log("C"); res.send("ok"); }); app.use((req, res, next) => { console.log("D"); next(); });সত্যিকারের Express 5.3.0 run:
--- /api/x A B C a 200 --- /home A D a 404/api/x-এ route উত্তর পাঠায় বলে D-তে পৌঁছায় না।/home-এ B-র path মেলে না, route-ও মেলে না, D চলে, তারপর সারি শেষ, তাই Express নিজের 404 দেয়। “a” দুই ক্ষেত্রেই আসে, কারণ এখানে সব synchronous. -
কোড সমালোচনা: নিচের ক্রমে কী কী ভুল আছে? ঠিক ক্রম লিখুন।
app.use(requireAuth); app.post("/api/orders", createOrder); app.use(express.json({ limit: "50mb" })); app.use(cors({ origin: true, credentials: true })); app.use(express.static("."));requireAuthসবার আগে: preflight 401 খাবে, আর public ফাইল বা পণ্য-তালিকাও লগইন চাইবে।- Route
express.json-এর আগে:req.bodyহবেundefined. 50mbসীমা: order-এর JSON-এ অকারণে বড় দরজা।corsroute-এর পরে, আরorigin: true: route-এর উত্তরে header-ই যাবে না, আর অনুমতিটি সবার জন্য খোলা।express.static("."): পুরো project folder ওয়েবে তোলে। Dotfile উপেক্ষিত হলেও source কোড আর অন্য ফাইল বেরিয়ে যেতে পারে। শুধুpublicদিন।
app.use(requestTimer); app.use("/api", cors({ origin: allowed, credentials: true })); app.use("/api", express.json({ limit: "10kb" })); app.use("/assets", express.static("public")); app.get("/api/products", listProducts); app.post("/api/orders", requireAuth, createOrder); -
নিজে চালান: একটি নতুন folder-এ
npm i express corsকরে এই পর্বেরcorsসেটআপ চালান।curl -si -X OPTIONSদিয়ে জানা আর অজানা দুই origin পাঠান। কোন header-টি পার্থক্য গড়ে?curl -si -X OPTIONS localhost:47326/api/orders \ -H 'Origin: http://evil.example' \ -H 'Access-Control-Request-Method: POST' \ -H 'Access-Control-Request-Headers: content-type'সত্যিকারের ফলে দুটিই
204 No Content. জানা origin-এAccess-Control-Allow-Origin: http://localhost:47327আছে, অজানাটিতে নেই;Vary: Origin,Allow-Methodsইত্যাদি দুটিতেই আছে। Status নয়, এই একটি header-ই browser-এর সিদ্ধান্ত ঠিক করে। Header মিলিয়ে দেখলে “server কি ঠিক আছে” বোঝা যায়, কিন্তু চূড়ান্ত প্রমাণ browser console.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি Express API-র middleware-এর ভালো ক্রম সাজান (▲▼ চেপে সরান)
requestTimer— সব অনুরোধের সময় ও logcors— preflight-এর উত্তর ও header- Rate limiter — অতিরিক্ত অনুরোধ থামানো
express.json— body পড়া, সীমাসহrequireAuth— ব্যবহারকারী চেনা- Route handler — আসল কাজ
একটি Express middleware next() ডাকল না, উত্তরও পাঠাল না। Client কী দেখবে?
- সাথে সাথে 404
- সাথে সাথে 500
- কোনো উত্তর আসে না, request ঝুলে থাকে
- পরের middleware নিজে থেকেই চলে
Express 5-এ app.post("/x", handler) লেখা হলো app.use(express.json())-এর আগে। JSON POST এলে handler-এ req.body-এর মান কী?
{}-
undefined - Parse করা object
- চালু হওয়ার সময়েই error
একটি API-তে cors শুধু একটি জানা origin-কে অনুমতি দেয়। অজানা origin বসিয়ে curl দিয়ে GET পাঠালে কী হবে?
- Server 403 ফেরত দেবে
- Connection কেটে যাবে
- curl একটি CORS error দেখাবে
- উত্তর আসবে, শুধু Allow-Origin header থাকবে না
React অ্যাপ fetch-এ credentials: "include" দিল, আর API উত্তর দিল Access-Control-Allow-Origin: * সহ। Browser কী করবে?
- উত্তর পড়তে দেবে না, CORS error দেখাবে
- Allow-Credentials true থাকলে পড়তে দেবে
- Cookie সরিয়ে আবার পাঠাবে
- স্বাভাবিকভাবে পড়তে দেবে
requireAuth বসানো হলো cors-এর আগে। সঠিক token থাকা সত্ত্বেও browser-এ JSON POST কেন ব্যর্থ হয়?
- Token-এর মেয়াদ শেষ
- JSON body খুব বড়
- Preflight OPTIONS-এ Authorization থাকে না, তাই auth সেটি থামিয়ে দেয়
- Express OPTIONS সমর্থন করে না
Express 5-এ একটি async middleware-এর ভেতরে await-এর পরে throw new Error("x") হলো, কোনো try/catch নেই। কী ঘটবে?
- Process পড়ে যাবে
- Error middleware-এ যাবে; নিজের handler না থাকলে ডিফল্ট 500
- Request চিরকাল ঝুলে থাকবে
- পরের স্বাভাবিক middleware চলবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Express মানে middleware-এর সারি; প্রতিটি
next()ডাকে, উত্তর দেয়, অথবা error পাঠায়। - ক্রমই আচরণ: cors আগে, parser তারপর, auth তারপর, route শেষে।
express.json-এ সীমা রাখুন; Express 5-এ parse না হলেreq.bodyহয়undefined.- CORS browser-এর নিয়ম, server-এর পাহারা নয়; credentials-এ
*চলে না, আরorigin: trueএড়িয়ে চলুন। - উত্তর একবার,
return res...; Express 5 rejected promise ধরে, কিন্তু ভুলে যাওয়াnextকেউ ধরে না।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৭ · Request যাচাই — Zod দিয়ে schema আর নিরাপদ ইনপুটপরবর্তী পর্বআজ
express.jsonJSON পড়ে দিল, এবার সেই ইনপুট বিশ্বাস করার আগে যাচাই করবেন। - ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপরের পর্ব ২৭: Request যাচাই, Zod দিয়ে schema আর নিরাপদ ইনপুট।
- পর্ব ০২ · একটি অনুরোধের পুরো যাত্রাপেছনে ফিরেApp server-এর সামনে-পেছনের পুরো স্তরগুলো।
- Web-এর নিরাপত্তা মডেলনিরাপত্তাSame-origin policy আর CORS-এর ভুল configuration-এর পুরো তত্ত্ব।
- Web hardeningনিরাপত্তাSecurity header চালুর ধাপ আর নিরাপদ error.
- Express দিয়ে REST APIপাশের ট্র্যাকJavaScript কোর্সের প্রথম পরিচয়, ভাষা শেখার দৃষ্টিতে।