এই পর্বে যা শিখবেন
route.ts-এ method, dynamic param, JSON, form data, cookie আর header নিয়ে কাজ করা- Zod আর problem details দিয়ে Next.js-এর API-তে Express-এর মতোই শক্ত চুক্তি রাখা
- Next.js 16-এ GET handler কখন cache হয়, build-এর route table পড়ে তা বোঝা
- Payment gateway-র webhook HMAC আর raw body দিয়ে যাচাই করে একবারই প্রক্রিয়া করা
proxy.ts-এর matcher, redirect, header আর rewrite, আর এর সীমা- Route handler, server action আর আলাদা Express API-র মধ্যে বেছে নেওয়া
দামের একক নিয়ে: এই পর্বের ছোট ল্যাবে উদাহরণ সহজ রাখতে দাম পূর্ণ টাকায় রাখা হয়েছে, আর আউটপুটগুলোও সেই run-এর। আসল দোকানে টাকা রাখুন পয়সায়, পূর্ণসংখ্যা হিসেবে, যেমনটি পর্ব ৩২-এ দেখেছেন।
১ · Next.js-এর ভেতরেই backend — route.ts কী
“বাজারঘর”-এর পাতাগুলো এখন Next.js-এ। কিন্তু দোকানের কাজ শুধু পাতা দেখানো নয়। মোবাইল অ্যাপকে পণ্যের তালিকা দিতে হয়। পেমেন্ট সফল হলে payment gateway নিজে এসে খবর দেয়। যোগাযোগের form-এর ডেটা নিতে হয়। এসবের জন্য পাতা নয়, JSON বা সাধারণ HTTP উত্তর লাগে। মডিউল ৪-এ এই কাজ করেছিলেন Express দিয়ে। Next.js-এ একই কাজ হয় route handler দিয়ে।
একটি বড় রেস্তোরাঁর কথা ভাবুন। হলঘরে ওয়েটার খাবার সাজিয়ে টেবিলে দেন, এটি page.tsx. পেছনের দরজায় আরেকটি জানালা আছে। সেখানে ডেলিভারির রাইডার এসে প্যাকেট নেন, সরবরাহকারী চালের বস্তা দিয়ে রসিদ নেন। এই জানালায় সাজানো প্লেট নেই, শুধু লেনদেন। এটিই route.ts. আর মূল ফটকের দারোয়ান সবাইকে ঢোকার আগে দেখেন: টিকিট আছে? ভুল দরজায় এসেছেন? এই দারোয়ান হলো proxy.ts, যার পুরোনো নাম middleware. দারোয়ান রান্না করেন না, শুধু দ্রুত পথ দেখান।
নিয়মটি সরল। app ফোল্ডারের ভেতরে যে ফোল্ডারে route.ts রাখবেন, সেই path-এ একটি API তৈরি হবে। ফাইলে HTTP method-এর নামে ফাংশন export করুন: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. Route handler প্রথম আসে Next.js 13.2-এ (২০২৩), App Router-এর অংশ হিসেবে। পুরোনো Pages Router-এর pages/api এর পূর্বসূরি। File-ভিত্তিক routing-এর বিস্তার পর্ব ৫১-এ (Next.js App Router) দেখবেন; এখানে শুধু API-র দিকটি।
import { NextRequest } from 'next/server';
import { db } from '@/lib/store';
export async function GET(request: NextRequest) {
const q = request.nextUrl.searchParams.get('q')?.toLowerCase() ?? '';
const items = db.products.filter((p) => p.name.toLowerCase().includes(q));
return Response.json({ items, count: items.length });
}এখানে দুটি জিনিস Express থেকে আলাদা। প্রথমত, ফাংশন একটি Request নেয় আর একটি Response ফেরত দেয়; res.send() ডাকার কিছু নেই। দ্বিতীয়ত, এই Request আর Response Next.js-এর বানানো নয়। এগুলো ব্রাউজারের Fetch API-র সেই একই class, যা WHATWG-এর Fetch মানে লেখা, আর Node.js 18 থেকে global হিসেবে পাওয়া যায়। Next.js শুধু এর ওপর NextRequest (বাড়তি nextUrl, cookies) আর NextResponse (বাড়তি redirect, rewrite, cookies) দেয়। সাধারণ কাজে Web-এর নিজের class-ই যথেষ্ট।
বাজারঘরে একটি অনুরোধ কোন পথে যায়
Where proxy.ts, route.ts and page.tsx sit
একটি নিয়ম মনে রাখুন: একই path-এ page.tsx আর route.ts দুটি একসাথে থাকতে পারে না। app/page.tsx আর app/route.ts সংঘাত; app/page.tsx আর app/api/route.ts ঠিক আছে। তাই প্রচলিত রীতি হলো API-গুলো app/api/...-এর নিচে রাখা। Route handler layout-এ অংশ নেয় না, client-side navigation-এও নয়; এটি সবচেয়ে নিচের স্তরের routing.
Next.js চালানো যায় না এই পাতায়। কিন্তু একটি route handler আসলে সাধারণ JavaScript ফাংশন। তাই নিচের ব্লকে শিক্ষার একটি মডেল: হুবহু একই আকৃতির GET, যাকে আমরা নিজে একটি Web Request দিয়ে ডাকছি। Node-এর নিজের Request আর Response, কোনো package নেই।
// শিক্ষার মডেল: route.ts-এর GET যেমন Request পায় আর Response ফেরত দেয়
const products = [
{ id: 1, name: "Miniket chal 5kg", price: 420 },
{ id: 2, name: "Masur dal 1kg", price: 135 },
];
function problem(status, title, detail) {
return Response.json({ type: "about:blank", title, status, detail }, {
status, headers: { "Content-Type": "application/problem+json" },
});
}
// app/api/products/[id]/route.ts-এর GET-এর মতো
async function GET(request, { params }) {
const { id } = await params; // Next.js 15+ এ params একটি Promise
const n = Number(id);
if (!Number.isInteger(n) || n < 1) return problem(400, "Bad Request", "id must be a positive integer");
const p = products.find((x) => x.id === n);
if (!p) return problem(404, "Not Found", `No product with id ${n}`);
const fields = new URL(request.url).searchParams.get("fields");
return Response.json(fields === "name" ? { name: p.name } : p);
}
async function call(path, id) {
const req = new Request("http://bazarghar.test" + path);
const res = await GET(req, { params: Promise.resolve({ id }) });
console.log(res.status, res.headers.get("content-type"), await res.text());
}
(async () => {
await call("/api/products/2", "2");
await call("/api/products/2?fields=name", "2");
await call("/api/products/9", "9");
await call("/api/products/abc", "abc");
})();চারটি লাইন, চারটি সিদ্ধান্ত। 200 application/json {"id":2,"name":"Masur dal 1kg","price":135}: সাধারণ সফলতা। দ্বিতীয় লাইনে ?fields=name query পড়ে শুধু {"name":"Masur dal 1kg"}. তৃতীয়টি 404 application/problem+json, আর চতুর্থটি 400: "abc" সংখ্যা নয়। লক্ষ করুন, Response.json() নিজেই application/json বসায়, আর আমরা চাইলে header দিয়ে তা বদলাতে পারি। Handler-টি একটি বিশুদ্ধ ফাংশন: Request ঢোকে, Response বেরোয়। এ কারণেই route handler-এর unit test লেখা সহজ, server চালাতে হয় না।
এই পর্বের সব দাবি একটি সত্যিকারের ল্যাব থেকে। লেখার সময়ের version: Next.js 16.4.4 (npm-এর latest), React 19.3.1, Zod 4.6.7, TypeScript 6.0.3, Node 24.15.0. প্রকল্পের path ছোট করে ~/bazarghar লেখা হয়েছে। Server চলেছে next build আর next start দিয়ে, শুধু 127.0.0.1-এ। Version বদলায়, তাই নিজের package.json মিলিয়ে নেবেন।
২ · Method, dynamic param আর ইনপুট পড়া — JSON, form, cookie, header
Express-এ ডেটা আসত তিন জায়গা থেকে: req.params, req.query, req.body (পর্ব ২৫)। Next.js-এ জায়গাগুলো একই, শুধু পড়ার পথ আলাদা। নিচের সারণিটি একবার মিলিয়ে নিন।
| কী পড়বেন | Express 5 | Next.js 16 route handler |
|---|---|---|
| Path-এর অংশ | req.params.id | (await ctx.params).id |
| Query | req.query.q | request.nextUrl.searchParams.get('q') |
| JSON body | express.json() + req.body | await request.json() |
| Form data | urlencoded বা multer | await request.formData() |
| Raw body | express.raw() | await request.text() |
| Cookie | cookie-parser | request.cookies বা await cookies() |
| Header | req.get('x') | request.headers.get('x') বা await headers() |
Body পড়ার জন্য কোনো parser middleware লাগে না, কারণ Web Request নিজেই json(), formData(), text() জানে। তবে একটি body একবারই পড়া যায়, কারণ এটি একটি stream. text() পড়ার পর আবার json() ডাকলে error হবে। Webhook-এ এই কথাটি খুব কাজে লাগবে।
Dynamic param: [id] আর async params
ফোল্ডারের নাম বন্ধনীতে লিখলে সেটি route param: app/api/products/[id]/route.ts. দ্বিতীয় argument-এর params Next.js 15 থেকে একটি Promise, তাই await লাগে। পুরোনো tutorial-এ params.id সরাসরি পড়া দেখবেন; Next.js 16-এ সেই synchronous পথ আর নেই। TypeScript-এ RouteContext নামের একটি global helper আছে, যা next build বা next dev চালানোর সময় তৈরি হয়, আর path থেকে param-এর type বানিয়ে দেয়:
import { db, problem } from '@/lib/store';
export async function GET(_req: Request, ctx: RouteContext<'/api/products/[id]'>) {
const { id } = await ctx.params; // id: string
const n = Number(id);
if (!Number.isInteger(n) || n < 1) return problem(400, 'Bad Request', 'id must be a positive integer');
const product = db.products.find((p) => p.id === n);
if (!product) return problem(404, 'Not Found', `No product with id ${n}`);
return Response.json(product);
}Param সবসময় string, ঠিক Express-এর মতো। "2" আর 2 এক নয়, তাই সংখ্যায় বদলে যাচাই করা হয়েছে। সত্যিকারের run (Next.js 16.4.4, next start, অপ্রাসঙ্গিক header বাদ):
$ curl -si http://127.0.0.1:47857/api/products/2
HTTP/1.1 200 OK
x-request-id: 777d17aa-4420-4d27-abec-5b939f70114e
vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch
content-type: application/json
{"id":2,"name":"Masur dal 1kg","price":135,"stock":12}
$ curl -si -X DELETE http://127.0.0.1:47857/api/products
HTTP/1.1 405 Method Not Allowed
$ curl -si -X OPTIONS http://127.0.0.1:47857/api/products
HTTP/1.1 204 No Content
allow: GET, HEAD, OPTIONS, POST
তিনটি জিনিস লক্ষ করুন। x-request-id এসেছে আমাদের proxy থেকে (৭ ও ৮ অংশে দেখবেন)। ফাইলে DELETE export করা নেই, তাই Next.js নিজেই 405 দিয়েছে। আর OPTIONS না লিখলেও Next.js নিজে একটি বানিয়ে allow header-এ বৈধ method জানিয়েছে। GET থাকলে HEAD-ও পাওয়া যায়। (HTTP-র নিয়মে 405-এর সাথে Allow header পাঠানোর কথা; আমাদের run-এ 405-এর উত্তরে তা ছিল না। কঠোর API চাইলে নিজে বসাতে পারেন।)
Form data, cookie আর header
যোগাযোগের form multipart/form-data বা application/x-www-form-urlencoded হিসেবে আসে। request.formData() দুটিই পড়ে। FormData-র সব মান string (বা ফাইল), তাই সংখ্যা চাইলে নিজে বদলাতে হয়।
import { z } from 'zod';
import { problem } from '@/lib/store';
const Contact = z.object({
name: z.string().trim().min(2).max(60),
phone: z.string().regex(/^01[3-9]\d{8}$/, 'Bangladeshi mobile number expected'),
});
export async function POST(request: Request) {
const form = await request.formData();
const parsed = Contact.safeParse(Object.fromEntries(form));
if (!parsed.success) return problem(422, 'Validation failed', parsed.error.issues[0].message);
return Response.json({ ok: true, name: parsed.data.name });
}Cookie আর header পড়ার দুটি পথ। request.cookies আর request.headers সরাসরি request থেকে। অথবা next/headers-এর cookies() আর headers(), যা Next.js 15 থেকে async. দ্বিতীয় পথটি server component আর server action-এও একই রকম কাজ করে, তাই ভাগ করা helper লিখতে সুবিধা।
import { cookies, headers } from 'next/headers';
import { problem } from '@/lib/store';
export async function GET() {
const cookieStore = await cookies();
const h = await headers();
const sid = cookieStore.get('sid')?.value;
if (!sid) return problem(401, 'Unauthorized', 'Please log in');
return Response.json({
user: sid === 'demo-session' ? 'rahima' : 'unknown', // ল্যাবের নকল session
requestId: h.get('x-request-id'),
lang: h.get('accept-language') ?? 'none',
});
}$ curl -s http://127.0.0.1:47857/api/contact -F name=Rahima -F phone=01712345678
{"ok":true,"name":"Rahima"}
$ curl -s http://127.0.0.1:47857/api/contact -F name=Rahima -F phone=12345
{"type":"about:blank","title":"Validation failed","status":422,"detail":"Bangladeshi mobile number expected"}
$ curl -s http://127.0.0.1:47857/api/me
{"type":"about:blank","title":"Unauthorized","status":401,"detail":"Please log in"}
$ curl -s http://127.0.0.1:47857/api/me -H 'Cookie: sid=demo-session' -H 'Accept-Language: bn-BD'
{"user":"rahima","requestId":"ec5a9b33-9d3f-4b48-9cf8-43e5597e84d7","lang":"bn-BD"}
ল্যাবে sid=demo-session একটি নকল মান, শুধু পথ দেখানোর জন্য। আসল session কীভাবে তৈরি আর যাচাই হয়, তা পর্ব ৪৫ আর পর্ব ৪৬-এ। Route handler শুধু সেই যাচাইকে ডাকবে। নিচের stepper-এ এই শেষ অনুরোধটি proxy থেকে handler পর্যন্ত ধাপে ধাপে দেখুন।
- curl
GET /api/mecookie সহ - matcher
/api/meমিলেছে - proxy
next()header যোগ - route.ts
GETcookie পড়া - Response
200JSON
GET /api/me HTTP/1.1
Cookie: sid=demo-session
Accept-Language: bn-BD
matcher: /((?!_next/static|…).*)
/api/me → মিলেছে
[proxy] GET /api/me runtime=nodejs 24.15.0
headers.set('x-request-id', id)
NextResponse.next({ request: { headers } })
res.headers.set('x-request-id', id)
id = ec5a9b33-9d3f-4b48-9cf8-43e5597e84d7
(await cookies()).get('sid') → demo-session
(await headers()).get('x-request-id')
→ ec5a9b33-… (proxy বসিয়েছে)
accept-language → bn-BD
{"user":"rahima",
"requestId":"ec5a9b33-9d3f-4b48-9cf8-43e5597e84d7",
"lang":"bn-BD"}
- অনুরোধ আসে। Cookie-তে
sid=demo-session, ভাষাbn-BD. Browser হলে cookie নিজে থেকেই যেত; curl-এ আমরা হাতে দিয়েছি। - Matcher আগে প্রশ্ন করে: proxy চলবে কি?
/api/mestatic ফাইল নয়, তাই নিয়মটি মেলে। Log-এ দেখা যায় proxy Node.js 24.15.0-এ চলছে। - Proxy একটি request id বসায়।
NextResponse.next({ request: { headers } })handler-এর জন্য request-এ header যোগ করে, আরres.headers.setclient-এর উত্তরে। এই অনুরোধের id:ec5a9b33-…. - Handler পড়ে।
cookies()থেকেdemo-session,headers()থেকে proxy-র বসানো একই id. Proxy আর handler-এর মধ্যে কথা বলার পথ এটিই: header, cookie বা URL. - উত্তর ফেরে।
200, JSON-এrequestIdঠিক সেইec5a9b33-…. Log আর উত্তর মেলাতে এই id কাজে লাগে, ঠিক পর্ব ৩১-এর মতো।
৩ · Status code, problem details আর Zod দিয়ে যাচাই
Framework বদলালেও API-র চুক্তি বদলায় না। পর্ব ২৭-এ শিখেছেন: server-এ পৌঁছানো কোনো কিছু বিশ্বাস নয়, Zod schema দিয়ে যাচাই। পর্ব ৩৩-এ শিখেছেন: ভুলের উত্তর এক ছাঁচে, problem details (RFC 9457). Next.js-এ শুধু এগুলো Response দিয়ে লিখতে হয়। ল্যাবে একটি ছোট helper সব route-এ ভাগ করা:
export function problem(status: number, title: string, detail: string, extra: Record<string, unknown> = {}) {
return Response.json(
{ type: 'about:blank', title, status, detail, ...extra },
{ status, headers: { 'Content-Type': 'application/problem+json' } },
);
}এবার পণ্য যোগ করার POST. ক্রম গুরুত্বপূর্ণ: আগে body JSON কি না, তারপর schema, তারপর কাজ।
const NewProduct = z.object({
name: z.string().trim().min(2).max(80),
price: z.number().int().positive().max(1_000_000),
stock: z.number().int().min(0).default(0),
});
export async function POST(request: Request) {
let body: unknown;
try {
body = await request.json();
} catch {
return problem(400, 'Bad Request', 'Body must be valid JSON');
}
const parsed = NewProduct.safeParse(body);
if (!parsed.success) {
return problem(422, 'Validation failed', 'Some fields are invalid', {
errors: parsed.error.issues.map((i) => ({ path: i.path.join('.'), message: i.message })),
});
}
const product = { id: db.nextId++, ...parsed.data }; // শুধু যাচাই করা ঘর
db.products.push(product);
return Response.json(product, { status: 201, headers: { Location: `/api/products/${product.id}` } });
}সত্যিকারের run, তিনটি অনুরোধ:
$ curl -si -X POST http://127.0.0.1:47857/api/products -H 'Content-Type: application/json' \
-d '{"name":"Chini 1kg","price":140,"stock":20}'
HTTP/1.1 201 Created
content-type: application/json
location: /api/products/4
{"id":4,"name":"Chini 1kg","price":140,"stock":20}
$ curl -si -X POST … -d '{"name":"X","price":-5}'
HTTP/1.1 422 Unprocessable Entity
content-type: application/problem+json
{"type":"about:blank","title":"Validation failed","status":422,"detail":"Some fields are invalid",
"errors":[{"path":"name","message":"Too small: expected string to have >=2 characters"},
{"path":"price","message":"Too small: expected number to be >0"}]}
$ curl -s -X POST … -d '{bad json'
{"type":"about:blank","title":"Bad Request","status":400,"detail":"Body must be valid JSON"}
লক্ষ করুন, {bad json-এ request.json() throw করে। try না থাকলে Next.js একটি সাধারণ 500 দিত, যা আসলে client-এর ভুল, server-এর নয়। Zod 4-এর বার্তা (Too small: expected …) Zod 3-এর থেকে আলাদা, তাই পুরোনো উদাহরণের লেখা হুবহু মিলবে না। ...parsed.data নিরাপদ, কারণ Zod-এর object অজানা key ফেলে দেয়; কেউ "id": 1 বা "isAdmin": true পাঠালেও তা ঢুকবে না (mass assignment, পর্ব ২৭)।
Status-এর ছোট তালিকা, route handler-এ: তৈরি হলে 201 আর Location; JSON-ই না হলে 400; JSON ঠিক কিন্তু নিয়ম ভাঙলে 422; login নেই 401; login আছে কিন্তু অনুমতি নেই 403; জিনিস নেই 404; method নেই 405 (Next.js নিজে দেয়)। প্রতিটির পূর্ণ যুক্তি পর্ব ৩৩-এ।
৪ · GET handler-এর cache — Next.js 16-এ আসলে কী হয়
খেয়াল রাখুন: “খালি config” মানে next.config.ts-এ কিছু চালু নেই। create-next-app-এর template কিন্তু cacheComponents চালু করেই প্রকল্প বানায় (পর্ব ৫১), তাই নতুন প্রকল্পে নিচের দ্বিতীয় আচরণটিই পাবেন।
এখানে পুরোনো tutorial সবচেয়ে বেশি ভুল পথে নেয়। Next.js 13 আর 14-এ request না পড়া একটি GET handler build-এর সময় একবার চলে static হয়ে যেত। অনেকে অবাক হতেন: “আমার API তো একই উত্তর দিচ্ছে!” Next.js 15-এ এই ডিফল্ট উল্টে যায়: GET handler আর ডিফল্টে cache হয় না। Next.js 16-এ কী হয়, docs পড়ার পর নিজে মেপে দেখি। দুটি প্রায় একই ফাইল:
export async function GET() {
return Response.json({ at: new Date().toISOString() });
}export const dynamic = 'force-static';
export async function GET() {
return Response.json({ at: new Date().toISOString() });
}সাথে একটি তৃতীয়, app/api/info/route.ts, যা শুধু {"shop":"Bazarghar","currency":"BDT"} ফেরত দেয়; এতে সময় বা request কিছুই নেই। সত্যিকারের next build, খালি config (cacheComponents বন্ধ) (route table থেকে পাতাগুলো বাদ দেওয়া হয়নি):
▲ Next.js 16.4.4 (Turbopack)
✓ Compiled successfully in 1305ms
…
Route (app)
┌ ○ /
├ ○ /_not-found
├ ○ /account
├ ƒ /api/clock
├ ○ /api/clock-static
├ ƒ /api/contact
├ ƒ /api/info
├ ƒ /api/me
├ ƒ /api/mobile/products
├ ƒ /api/products
├ ƒ /api/products/[id]
├ ƒ /api/webhooks/payment
├ ○ /login
├ ○ /offer/a
└ ○ /offer/b
ƒ Proxy (Middleware)
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
চিহ্নগুলো পড়ুন। ○ মানে build-এর সময় তৈরি, ƒ মানে প্রতি অনুরোধে চলে। /api/info-তে কোনো পরিবর্তনশীল কিছুই নেই, তবু ƒ. অর্থাৎ ডিফল্টে কোনো GET handler cache হয়নি। শুধু force-static দেওয়াটি ○. নিচে Proxy-র লাইনেও লক্ষ করুন, “Proxy (Middleware)”: নতুন নাম, পুরোনোটি বন্ধনীতে। next start-এর পর দুবার করে ডাকলে:
$ curl -s http://127.0.0.1:47857/api/clock ; curl -s http://127.0.0.1:47857/api/clock
{"at":"2026-11-02T17:33:38.443Z"}
{"at":"2026-11-02T17:33:38.461Z"}
$ curl -s http://127.0.0.1:47857/api/clock-static ; curl -s http://127.0.0.1:47857/api/clock-static
{"at":"2026-11-02T17:33:26.163Z"}
{"at":"2026-11-02T17:33:26.163Z"}
$ curl -si http://127.0.0.1:47857/api/clock-static | grep -i cache
x-nextjs-cache: HIT
cache-control: s-maxage=31536000
প্রথমটি প্রতিবার নতুন সময়, ১৮ মিলিসেকেন্ডের ব্যবধানে। দ্বিতীয়টি দুবারই 17:33:26.163: এটি build-এর মুহূর্ত, server চালুর আগের সময়। x-nextjs-cache: HIT বলছে উত্তর cache থেকে এসেছে, আর s-maxage=31536000 (৩৬৫ দিন) CDN-এর মতো shared cache-কেও রাখতে বলছে। force-static-এ handler-এর ভেতরে request.headers বা cookie পড়লে খালি মান পাবেন, কারণ build-এর সময় কোনো আসল অনুরোধ থাকে না। তাই এটি শুধু সবার জন্য একই উত্তরের API-তে মানায়, যেমন জেলার তালিকা।
Cache Components চালু থাকলে
Next.js 16-এর নতুন মডেল Cache Components (next.config.ts-এ cacheComponents: true). Docs অনুযায়ী তখন GET handler সাধারণ পাতার মতো আচরণ করে: অনিশ্চিত বা অনুরোধ-নির্ভর কিছু না ছুঁলে build-এর সময় prerender হয়। একই প্রকল্পে চালু করে আবার build:
- Cache Components enabled
- Partial Prefetching enabled
Error: Route segment config "dynamic" is not compatible with `nextConfig.cacheComponents`. Please remove it.
(clock-static সরিয়ে আবার build)
├ ƒ /api/clock
├ ○ /api/info
├ ƒ /api/me
…
এবার /api/info ○, কারণ এটি কিছুই পড়ে না। /api/clock ƒ, কারণ new Date() অনিশ্চিত। আর পুরোনো dynamic config এই মোডে নিষিদ্ধ, build থেমে যায়। একটি সতর্কতাও এসেছিল: cacheComponents-এর সাথে partialPrefetching-ও লিখতে বলছে, আর পরের major version-এ দুটিই সর্বত্র চালু হবে বলে জানাচ্ছে। Database-এর ডেটা cache করতে হলে docs-এর পথ হলো একটি আলাদা helper ফাংশনে 'use cache' আর cacheLife; সরাসরি handler-এর ভেতরে নয়। Cache-এর পুরো মডেল পর্ব ৫৩ (Data fetching আর cache)-এর বিষয়, rendering-এর ছক পর্ব ৫৫-এর।
মূল কথা
Next.js 16, খালি config (cacheComponents বন্ধ): GET handler প্রতি অনুরোধে চলে। Cache চাইলে স্পষ্ট করে বলুন। POST, PUT, DELETE কখনো cache হয় না, একই ফাইলে cache করা GET থাকলেও। এবং যে কোনো দাবি নিজের next build-এর route table দিয়ে মিলিয়ে নিন।
৫ · Webhook — HMAC, raw body আর একবারই জমা
ক্রেতা বিকাশ বা কার্ডে টাকা দিলেন। Gateway পরে নিজে এসে বাজারঘরের একটি URL-এ POST করে জানায়: “order BG-2041-এর টাকা এসেছে”। একেই বলে webhook. সমস্যা হলো, URL-টি তো খোলা। যে কেউ সেখানে একই আকৃতির JSON পাঠাতে পারেন। তাই প্রশ্ন একটিই: এই বার্তা কি সত্যিই gateway-র, আর পথে কেউ বদলায়নি তো?
পুরোনো দিনে জমিদারি কাছারিতে চিঠি আসত গালার সিলমোহর দিয়ে। সিল ভাঙা থাকলে চিঠি অগ্রাহ্য। Webhook-এর স্বাক্ষর সেই সিল, তবে গাণিতিক। দোকান আর gateway দুজনেই একটি গোপন key জানেন। Gateway বার্তার প্রতিটি byte আর key মিলিয়ে HMAC বানিয়ে header-এ পাঠায়। দোকান একই হিসাব নিজে করে মেলায়। একটি অক্ষর বদলালেও সিল মিলবে না। আর key ছাড়া নতুন সিল বানানো যায় না।
এখানে কোনো আসল gateway-র API নকল করা হয়নি। প্রতিটি gateway-র header-এর নাম, স্বাক্ষরের ধরন আর retry-র নিয়ম আলাদা; সেগুলো তাদের নিজের docs থেকে নেবেন। ল্যাবে আমরা একটি কাল্পনিক “BazarPay” ধরেছি, যার header x-bazarpay-signature: t=<সময়>,v1=<hex>, আর স্বাক্ষর হয় HMAC-SHA256(secret, t + "." + rawBody). অনেক বাস্তব gateway এর কাছাকাছি ধরন ব্যবহার করে। Secret ল্যাবে example-webhook-secret, .env.local-এ, কোডে নয়।
import { createHmac, timingSafeEqual } from 'node:crypto';
import { z } from 'zod';
import { db, problem } from '@/lib/store';
const SECRET = process.env.PAYMENT_WEBHOOK_SECRET ?? '';
const TOLERANCE_SEC = 300;
const Event = z.object({ id: z.string().min(1), type: z.literal('payment.succeeded'),
orderId: z.string().min(1), amount: z.number().int().positive() });
function validSignature(header: string | null, raw: string, nowSec: number) {
if (!header || !SECRET) return false;
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=') as [string, string]));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(nowSec - t) > TOLERANCE_SEC) return false;
const expected = createHmac('sha256', SECRET).update(`${t}.${raw}`).digest();
const given = Buffer.from(parts.v1 ?? '', 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}
export async function POST(request: Request) {
const raw = await request.text(); // raw body: যাচাইয়ের আগে parse নয়
if (!validSignature(request.headers.get('x-bazarpay-signature'), raw, Math.floor(Date.now() / 1000))) {
return problem(401, 'Invalid signature', 'Webhook signature check failed');
}
const parsed = Event.safeParse(JSON.parse(raw));
if (!parsed.success) return problem(422, 'Bad event', 'Unexpected event shape');
const ev = parsed.data;
if (db.seenEvents.has(ev.id)) return Response.json({ received: true, duplicate: true });
db.seenEvents.add(ev.id); // বাস্তবে: unique column, একই transaction-এ
db.paidOrders.set(ev.orderId, ev.id);
return Response.json({ received: true, duplicate: false });
}চারটি সিদ্ধান্ত আলাদা করে দেখুন।
- Raw body. স্বাক্ষর হয় byte-এর ওপর।
request.json()দিয়ে parse করে আবারJSON.stringifyকরলে space, key-এর ক্রম বা সংখ্যার লেখা বদলাতে পারে, আর স্বাক্ষর মিলবে না। তাইrequest.text(). Express-এ এর জন্য আলাদাexpress.raw()লাগত; route handler-এ body কেউ আগে পড়ে না, তাই সমস্যা নেই। - সমান সময়ের তুলনা।
===দিয়ে string মেলালে প্রথম অমিলেই থামে, আর সময়ের পার্থক্য থেকে তথ্য ফাঁস হতে পারে (timing leak).timingSafeEqualসমান দৈর্ঘ্যের দুটি buffer পুরোটা তুলনা করে; দৈর্ঘ্য আলাদা হলে এটি throw করে, তাই আগে দৈর্ঘ্য মেলানো। - সময়ের সীমা। একটি বৈধ বার্তা কেউ হাতে পেলে পরে হুবহু আবার পাঠাতে পারেন: replay.
tস্বাক্ষরের ভেতরে, তাই বদলানো যায় না, আর ৩০০ সেকেন্ডের বেশি পুরোনো হলে বাতিল। - একবারই কাজ। Gateway নিজেও একই ঘটনা একাধিকবার পাঠাতে পারে, যেমন আপনার উত্তর সময়মতো না পৌঁছালে retry. তাই event id দেখে দ্বিতীয়বার কিছুই করা হয় না, কিন্তু
200দেওয়া হয়, যাতে gateway retry থামায়। এটি পর্ব ৩৩-এর idempotency-র একই নীতি, এখানে key দিচ্ছে gateway নিজে।
নিচের ব্লকে একই যুক্তি, নির্দিষ্ট সময় দিয়ে, যাতে ফল সবসময় এক থাকে। শেষ লাইনটি সবচেয়ে শিক্ষণীয়।
const { createHmac, timingSafeEqual } = require("node:crypto");
const SECRET = "example-webhook-secret"; // নকল মান
const seen = new Set(); // বাস্তবে database-এর unique column
const sign = (t, raw) => createHmac("sha256", SECRET).update(`${t}.${raw}`).digest("hex");
function verify(header, raw, now) {
const { t, v1 } = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
if (Math.abs(now - Number(t)) > 300) return "too old";
const expected = Buffer.from(sign(t, raw), "hex");
const given = Buffer.from(v1 ?? "", "hex");
const ok = given.length === expected.length && timingSafeEqual(given, expected);
return ok ? "ok" : "bad signature";
}
function handle(header, raw, now) {
const check = verify(header, raw, now);
if (check !== "ok") return `401 ${check}`;
const ev = JSON.parse(raw); // যাচাইয়ের পরেই parse
if (seen.has(ev.id)) return `200 duplicate ${ev.id}, nothing changed`;
seen.add(ev.id);
return `200 order ${ev.orderId} paid (${ev.amount} BDT)`;
}
const now = 1793640000; // নির্দিষ্ট সময়, যাতে ফল একই থাকে
const raw = '{"id":"evt_1001","orderId":"BG-2041","amount":1250}';
const header = `t=${now},v1=${sign(now, raw)}`;
const tampered = raw.replace("1250", "9");
console.log("1 valid :", handle(header, raw, now + 2));
console.log("2 retry :", handle(header, raw, now + 40));
console.log("3 tampered :", handle(header, tampered, now + 3));
console.log("4 replay :", handle(header, raw, now + 3600));
console.log("same JSON, new spacing:", handle(header, JSON.stringify(JSON.parse(raw), null, 1), now + 5));1 valid : 200 order BG-2041 paid (1250 BDT): স্বাক্ষর মিলেছে, প্রথমবার, তাই কাজ হলো। 2 retry : 200 duplicate evt_1001, nothing changed: ৪০ সেকেন্ড পরে একই বার্তা, স্বাক্ষর বৈধ, কিন্তু id আগে দেখা, তাই কিছু বদলায়নি। 3 tampered : 401 bad signature: কেউ 1250-কে 9 করেছেন, সিল ভেঙেছে। 4 replay : 401 too old: এক ঘণ্টা পরে হুবহু একই বার্তা, স্বাক্ষর ঠিক, কিন্তু সময়ের সীমা পার। আর শেষ লাইন 401 bad signature: একই JSON, শুধু space বদলেছে। অর্থের দিক থেকে কিছুই বদলায়নি, তবু byte আলাদা, তাই স্বাক্ষর মেলে না। Raw body কেন জরুরি, এটিই তার প্রমাণ।
এবার আসল Next.js server-এ। ল্যাবের একটি ছোট script (sign.mjs) একই নকল secret দিয়ে header বানায়। সত্যিকারের run:
$ SIG=$(node sign.mjs body.json); echo $SIG
t=1793640841,v1=8d785f56b5269fdc116bda3230e83c86443ce14b795b137808435dfecb3fcb66
$ curl -s -w ' [%{http_code}]\n' $URL -H "x-bazarpay-signature: $SIG" --data-binary @body.json
{"received":true,"duplicate":false} [200]
$ curl … একই অনুরোধ আবার
{"received":true,"duplicate":true} [200]
$ curl … একই header, tampered.json (amount 9)
{"type":"about:blank","title":"Invalid signature","status":401,"detail":"Webhook signature check failed"} [401]
$ curl … ৯০০ সেকেন্ড পুরোনো t দিয়ে স্বাক্ষর
… "Invalid signature" … [401]
$ curl … কোনো header ছাড়া
… "Invalid signature" … [401]
server log:
[webhook] order BG-2041 marked paid (event evt_1001)
পাঁচটি অনুরোধ, কিন্তু server log-এ marked paid মাত্র একবার। লক্ষ করুন, --data-binary ব্যবহার হয়েছে; শুধু -d দিলে curl ফাইলের newline ফেলে দিতে পারে, আর তখন byte বদলে যায়।
- Gateway
POSTস্বাক্ষরসহ - HMAC
text()byte মেলানো - জমা
evt_1001প্রথমবার - Retry
duplicateকিছু নয় - বদলানো
401বাতিল
x-bazarpay-signature: t=1793640841,
v1=8d785f56…cb66
{"id":"evt_1001","type":"payment.succeeded",
"orderId":"BG-2041","amount":1250}
raw = await request.text()
expected = HMAC(secret, "1793640841." + raw)
= 8d785f56…cb66
timingSafeEqual → true
|now − t| ≤ 300 → true
seenEvents = { evt_1001 }
paidOrders = { BG-2041 → evt_1001 }
200 {"received":true,"duplicate":false}
স্বাক্ষর → বৈধ
seenEvents.has("evt_1001") → true
200 {"received":true,"duplicate":true}
paidOrders অপরিবর্তিত
raw-এ "amount":9 expected ≠ v1 401 Invalid signature seenEvents, paidOrders অপরিবর্তিত
- Gateway পাঠায়। Header-এ সময়
t=1793640841আর স্বাক্ষর8d785f56…, body-তে eventevt_1001, orderBG-2041, ১২৫০ টাকা। - Server নিজে হিসাব করে। Raw body পড়ে,
"1793640841." + raw-এর HMAC বানায়: একই8d785f56…. সমান সময়ে তুলনাtrue, সময়ও সীমার ভেতরে। এখনো কিছু জমা হয়নি। - প্রথমবার, তাই কাজ।
evt_1001আগে দেখা হয়নি। Id জমা হয়, order paid হয়, উত্তর200আরduplicate:false. - Retry এলো। স্বাক্ষর আবার বৈধ, কিন্তু id আগে দেখা। কোনো পরিবর্তন নেই, তবু
200, যাতে gateway থামে। এখানে409দিলে অনেক gateway ব্যর্থতা ধরে আবার পাঠাত। - Body বদলানো। একই header, কিন্তু amount
9. হিসাব মেলে না,401. কোনো অবস্থা বদলায়নি, কারণ যাচাই হয়েছে parse-এর আগেই।
স্বাক্ষরের পরেও যা করবেন: (১) টাকার পরিমাণ আর মুদ্রা নিজের order-এর সাথে মেলান; স্বাক্ষর শুধু বলে বার্তা gateway-র, আপনার order-এর দাম ঠিক কি না নয়। (২) অনেক gateway একটি “যাচাই” API দেয়; বড় লেনদেনে webhook পেয়ে gateway-কে আলাদা করে জিজ্ঞেস করাই নিরাপদ পথ। (৩) seenEvents memory-তে রাখবেন না; server restart বা একাধিক কপিতে তা হারায়। Database-এ event id-র unique column, আর order আপডেট একই transaction-এ। (৪) ভারী কাজ (email, invoice) পরে queue-তে দিন, দ্রুত 200 ফেরত দিন (পর্ব ৬২)। পূর্ণ পেমেন্টের flow পর্ব ৬৫ (Payment integration)-এ।
৬ · মোবাইল অ্যাপের জন্য CORS
বাজারঘরের একটি মোবাইল অ্যাপও আছে। প্রথম প্রশ্ন: অ্যাপটি কী দিয়ে বানানো? এখানে একটি প্রচলিত ভুল ধারণা আছে। CORS browser-এর নিয়ম। Kotlin বা Swift-এ লেখা native অ্যাপ, বা React Native-এর সাধারণ network call, browser নয়; তারা CORS মানে না, preflight-ও পাঠায় না। তাদের জন্য CORS header দেওয়া বা না দেওয়া, কিছুই বদলায় না। কিন্তু অ্যাপটি যদি WebView-এ চলা web কোড হয়, বা m.bazarghar.test-এর মতো আলাদা origin-এর web অ্যাপ হয়, তখন browser-এর নিয়ম খাটে, আর CORS লাগবে। তত্ত্ব আছে পর্ব ২৬-এর CORS অংশে আর পর্ব ৫০-এ।
import { db } from '@/lib/store';
const ALLOWED = new Set(['https://m.bazarghar.test', 'http://localhost:8100']);
function corsHeaders(origin: string | null): Record<string, string> {
const h: Record<string, string> = { Vary: 'Origin' };
if (origin && ALLOWED.has(origin)) {
h['Access-Control-Allow-Origin'] = origin;
h['Access-Control-Allow-Methods'] = 'GET, OPTIONS';
h['Access-Control-Allow-Headers'] = 'Authorization, Content-Type';
h['Access-Control-Max-Age'] = '600';
}
return h;
}
export async function OPTIONS(request: Request) {
return new Response(null, { status: 204, headers: corsHeaders(request.headers.get('origin')) });
}
export async function GET(request: Request) {
const items = db.products.map(({ id, name, price }) => ({ id, name, price }));
return Response.json({ items }, { headers: corsHeaders(request.headers.get('origin')) });
}সত্যিকারের run, দুটি preflight:
$ curl -si -X OPTIONS http://127.0.0.1:47857/api/mobile/products \
-H 'Origin: https://m.bazarghar.test' -H 'Access-Control-Request-Method: GET' \
-H 'Access-Control-Request-Headers: authorization'
HTTP/1.1 204 No Content
access-control-allow-headers: Authorization, Content-Type
access-control-allow-methods: GET, OPTIONS
access-control-allow-origin: https://m.bazarghar.test
access-control-max-age: 600
$ curl -si -X OPTIONS … -H 'Origin: https://evil.example' -H 'Access-Control-Request-Method: GET'
HTTP/1.1 204 No Content
x-request-id: 2664dd47-42a3-4a65-abce-d2ccf8dd8d13
অচেনা origin-কে কোনো access-control-* header দেওয়া হয়নি, তাই browser তার পাতাকে উত্তর পড়তে দেবে না। তিনটি সিদ্ধান্ত লক্ষ করুন। * নয়, নির্দিষ্ট তালিকা। Vary: Origin, যাতে কোনো cache এক origin-এর উত্তর অন্যকে না দেয়। আর Authorization header-এর অনুমতি, কারণ মোবাইল অ্যাপ সাধারণত cookie নয়, token পাঠায় (পর্ব ৪৬)। মনে রাখুন, CORS server রক্ষা করে না। Curl বা native অ্যাপ যেকোনো origin লিখে পাঠাতে পারে। তাই অনুমতির আসল যাচাই token দিয়ে, handler-এ। অনেকগুলো route-এ একই CORS লাগলে docs-এর পরামর্শ: proxy-তে বা next.config-এর headers-এ এক জায়গায় লিখুন।
৭ · proxy.ts — middleware-এর নতুন নাম, matcher আর runtime
এবার দারোয়ান। Next.js 12-এ (২০২১) middleware.ts নামে একটি ফাইল আসে, যা route মেলার আগে চলে। Next.js 16-এ এর নাম বদলে হয়েছে proxy.ts, আর ফাংশনের নাম middleware থেকে proxy. কাজ একই। Docs-এ কারণ দুটি বলা হয়েছে। এক, “middleware” শব্দটি Express-এর middleware-এর সাথে গুলিয়ে যায়, যেখানে route-এর ভেতরে স্তরে স্তরে অনেক কাজ চলে (পর্ব ২৬)। Next.js-এর এই ফাইল আসলে অ্যাপের সামনে একটি network সীমানা, অনেকটা reverse proxy-র মতো। দুই, Next.js চায় এটি শেষ উপায় হিসেবে ব্যবহার হোক, তাই নামেই তা বোঝানো।
পুরোনো নামে কী হয়? ফাইলের নাম middleware.ts রেখে সত্যিকারের next build:
▲ Next.js 16.4.4 (Turbopack)
⚠ The "middleware" file convention is deprecated. Please use "proxy" instead.
To migrate automatically, run:
npx @next/codemod@canary middleware-to-proxy .
Learn more: https://nextjs.org/docs/messages/middleware-to-proxy
শুধু সতর্কতা, এখনো চলে। Codemod ফাইল আর ফাংশন দুটির নাম বদলে দেয়। কিন্তু এই run-এ আরও একটি জিনিস বেরিয়ে এলো, যা docs-এর version table থেকে অনুমান করা কঠিন।
কোন runtime-এ চলে — নিজে মেপে দেখা
ল্যাবের proxy শুরুতে নিজের runtime ছাপে: EdgeRuntime global থাকলে edge, না হলে Node-এর version. proxy.ts নামে চালালে log:
[proxy] GET /api/me runtime=nodejs 24.15.0
একই ফাইল middleware.ts নামে build করলে:
Error: Turbopack build failed with 1 error:
./middleware.ts:6:15
Error: A Node.js API is used (process.versions at line: 6) which is not supported in the Edge Runtime.
> 6 | : `nodejs ${process.versions.node}`;
process.versions সরিয়ে আবার চালালে log-এ runtime=edge. অর্থাৎ Next.js 16.4.4-এ proxy.ts ডিফল্টে Node.js runtime-এ চলে, আর পুরোনো middleware.ts এখনো Edge runtime-এ। Docs-এর version table বলে Next.js 15.5-এ middleware-এ Node.js runtime stable হয়েছিল (চাইলে বেছে নিতে হতো), আর 16-এ Proxy ডিফল্টে Node.js. আর proxy ফাইলে runtime config লেখা যায় না; docs অনুযায়ী লিখলে error হয়। শুধু নাম বদলালে তাই আচরণও বদলায়: Edge-এ যে node:crypto বা database driver চলত না, Node-এ তা চলবে। এটি সুবিধা, আবার ফাঁদও, কারণ “চলে” বলেই proxy-তে ভারী কাজ ঢোকানো লোভনীয় হয়ে ওঠে।
Matcher — কোন অনুরোধে proxy চলবে
Matcher না দিলে proxy প্রতিটি অনুরোধে চলে: পাতা, API, _next/static-এর JS আর CSS, ছবি, সব। একটি পাতা খুললেই তাই ডজনখানেক বার। Matcher লেখা হয় path-to-regexp-এর ধরনে, regex-সহ। ল্যাবের নিয়ম:
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|svg|css|js)$).*)'],
};(?!…) হলো negative lookahead: “এর পরে এগুলোর কোনোটি না থাকলে মেলাও”। নিয়মটি build-এর সময় পড়া হয়, তাই এখানে variable বা ফাংশন চলে না; শুধু ধ্রুব লেখা। নিচের ব্লকে একই regex-কে সাধারণ JavaScript-এ মেলানো হলো। এটি শিক্ষার মডেল: Next.js ভেতরে আরও কিছু করে (যেমন has/missing শর্ত, locale), কিন্তু path-এর অংশটি এভাবেই কাজ করে।
// proxy.ts-এর matcher, নিচে একটি সরল মডেলে পরীক্ষা
const source = "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|svg|css|js)$).*)";
const re = new RegExp("^" + source + "$");
const paths = [
"/",
"/account/orders",
"/api/products/2",
"/_next/static/chunks/main-abc123.js",
"/_next/image",
"/favicon.ico",
"/logo.png",
"/products/chal.png/reviews",
"/static-offer",
];
let runs = 0;
for (const p of paths) {
const hit = re.test(p);
if (hit) runs++;
console.log((hit ? "RUN " : "skip ") + p);
}
console.log(`proxy runs on ${runs} of ${paths.length} paths`);শেষ লাইন proxy runs on 5 of 9 paths. skip /_next/static/chunks/main-abc123.js, skip /_next/image, skip /favicon.ico, skip /logo.png: এগুলোই বাদ দিতে চেয়েছিলাম। দুটি সূক্ষ্ম লাইন দেখুন। RUN /products/chal.png/reviews: .png আছে, কিন্তু শেষে নয়, আর regex-এ $ শেষ বোঝায়, তাই এটি পাতা হিসেবেই ধরা হয়েছে। RUN /static-offer: নামে “static” থাকলেও এটি _next/static নয়। আর RUN /api/products/2: আমরা API বাদ দিইনি, কারণ request id সেখানেও চাই। Docs-এর উদাহরণে প্রায়ই api-ও বাদ থাকে; কোনটি বাদ দেবেন তা আপনার proxy কী করে তার ওপর নির্ভর করে।
আসল server-এ: curl /logo.png দিল 200, আর server log-এ ওই অনুরোধের কোনো [proxy] লাইন নেই। Docs-এর একটি সতর্কতা মনে রাখুন: matcher-এ _next/data বাদ দিলেও proxy সেখানে চলে, ইচ্ছা করেই, যাতে পাতা রক্ষা করে তার ডেটার পথ ভুলে খোলা না থাকে।
- HTML
/accountRUN - JS
_next/staticskip - ছবি
/logo.pngskip - API
/api/meRUN
"/account" → lookahead: _next/static? না
শেষে .png/.js? না → RUN
proxy চলেছে: 1 বাদ: 0
lookahead: _next/static → মিলেছে
→ skip, সরাসরি ফাইল
proxy চলেছে: 1 বাদ: 1
শেষে .png$ → skip
curl → 200, log-এ [proxy] লাইন নেই
proxy চলেছে: 1 বাদ: 2
কোনো বাদের শর্ত মেলেনি → RUN
[proxy] GET /api/me runtime=nodejs 24.15.0
proxy চলেছে: 2 বাদ: 2
- পাতার HTML.
/account-এ কোনো বাদের শর্ত মেলে না, তাই proxy চলে। এখানেই login-এর redirect দরকার। হিসাব: চলেছে ১, বাদ ০। - JavaScript chunk. Path শুরু হয়
_next/staticদিয়ে, lookahead মেলে, তাই proxy বাদ। এই ফাইলে auth-এর কিছু নেই, আর এগুলোই সংখ্যায় সবচেয়ে বেশি। চলেছে ১, বাদ ১। - ছবি। শেষে
.png, তাই বাদ। আসল run-এ200এসেছে, log-এ proxy-র কোনো লাইন নেই। চলেছে ১, বাদ ২। - API.
/api/meবাদ পড়ে না, proxy request id বসায়। মোট: চারটি অনুরোধের দুটিতে proxy চলেছে। Matcher না থাকলে চারটিতেই চলত।
৮ · Proxy-র কাজ: redirect, header, rewrite — আর যা করবেন না
ল্যাবের পুরো proxy.ts. তিনটি কাজ: login ছাড়া /account-এ এলে login পাতায় পাঠানো, /offer-এ A/B test, আর বাকি সবার জন্য একটি request id.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function proxy(request: NextRequest) {
const { pathname, search } = request.nextUrl;
const requestId = crypto.randomUUID();
// ১. login ছাড়া /account নয় (শুধু হালকা প্রাথমিক যাচাই)
const isAccount = pathname === '/account' || pathname.startsWith('/account/');
if (isAccount && !request.cookies.has('sid')) {
const login = new URL('/login', request.url);
login.searchParams.set('next', pathname + search);
return NextResponse.redirect(login);
}
// ২. A/B: URL থাকে /offer, ভেতরের পাতা /offer/a বা /offer/b
if (pathname === '/offer') {
const bucket = request.cookies.get('ab')?.value ?? (Math.random() < 0.5 ? 'a' : 'b');
const res = NextResponse.rewrite(new URL(`/offer/${bucket}`, request.url));
res.cookies.set('ab', bucket, { path: '/', maxAge: 60 * 60 * 24 * 30, sameSite: 'lax' });
return res;
}
// ৩. এগিয়ে যান, handler আর উত্তরে request id দিয়ে
const headers = new Headers(request.headers);
headers.set('x-request-id', requestId);
const res = NextResponse.next({ request: { headers } });
res.headers.set('x-request-id', requestId);
return res;
}প্রথম শর্তটি একসময় ছিল pathname.startsWith('/account'). নিচের ব্লকে দেখুন কেন বদলাতে হলো।
// শিক্ষার মডেল: proxy-র সিদ্ধান্ত, শুধু cookie আছে কি না দেখে
function proxy(request) {
const url = new URL(request.url);
const cookie = request.headers.get("cookie") ?? "";
const hasSid = /(?:^|;\s*)sid=/.test(cookie);
if (url.pathname.startsWith("/account") && !hasSid) {
const login = new URL("/login", url);
login.searchParams.set("next", url.pathname + url.search);
return Response.redirect(login, 307);
}
return null; // null মানে "এগিয়ে যান" (Next.js-এ NextResponse.next())
}
const cases = [
["/account/orders?page=2", ""],
["/account/orders?page=2", "theme=dark; sid=abc123"],
["/products", ""],
["/accounts-help", ""],
];
for (const [path, cookie] of cases) {
const req = new Request("http://bazarghar.test" + path, { headers: { cookie } });
const res = proxy(req);
const out = res ? `${res.status} -> ${res.headers.get("location")}` : "next()";
console.log(`${path.padEnd(24)} cookie=${(cookie || "-").padEnd(22)} ${out}`);
}প্রথম লাইন 307 -> http://bazarghar.test/login?next=%2Faccount%2Forders%3Fpage%3D2: cookie নেই, তাই login-এ, আর ফিরে আসার ঠিকানা encode করা, query-সহ। দ্বিতীয় লাইন next(): অন্য cookie-র মাঝে sid আছে। তৃতীয় /products next(). চতুর্থ লাইনটিই বাগ: /accounts-help একটি খোলা সাহায্যের পাতা, অথচ 307 -> …/login?next=%2Faccounts-help. startsWith("/account") “accounts”-কেও ধরেছে। ল্যাবের প্রথম proxy-তে হুবহু এই ভুল ছিল; আসল Next.js server-এ /accounts-help দিয়েছিল 307. ঠিক করার পর (=== '/account' অথবা '/account/' দিয়ে শুরু) একই অনুরোধ 200. Prefix মেলানোর সময় শেষে / রাখার অভ্যাস করুন। উল্টো ভুলটি আরও বিপজ্জনক: সুরক্ষার নিয়ম যত ঢিলা, তত পাতা খোলা থাকে।
আসল server-এর সত্যিকারের run:
$ curl -si "http://127.0.0.1:47857/account?tab=orders"
HTTP/1.1 307 Temporary Redirect
location: /login?next=%2Faccount%3Ftab%3Dorders
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:47857/account -H 'Cookie: sid=demo-session'
200
$ curl -si http://127.0.0.1:47857/offer
HTTP/1.1 200 OK
set-cookie: ab=a; Path=/; Expires=Wed, 02 Dec 2026 17:33:48 GMT; Max-Age=2592000; SameSite=lax
x-middleware-rewrite: http://localhost:47857/offer/a
…<h1>Offer variant A</h1>…
$ curl -s http://127.0.0.1:47857/offer -H 'Cookie: ab=b' | grep -o 'Offer variant [AB]' | head -1
Offer variant B
Build-এ /account ছিল ○, অর্থাৎ static HTML. তবু redirect কাজ করেছে, কারণ proxy চলে filesystem route-এর আগে। Docs-এর ক্রম: next.config-এর headers, তারপর redirects, তারপর proxy, তারপর beforeFiles rewrite, তারপর public/, _next/static, app/-এর route, তারপর dynamic route. Rewrite-এ browser-এর ঠিকানা /offer-ই থাকে, অথচ উত্তর আসে /offer/a থেকে; redirect-এর মতো দ্বিতীয় অনুরোধ লাগে না। Cookie থাকায় একই ক্রেতা বারবার একই রূপ দেখেন। একটি পর্যবেক্ষণ: ল্যাবের log-এ rewrite-এর পরে গন্তব্য /offer/a-এর জন্যও একটি [proxy] লাইন এসেছিল; proxy-র কাজ তাই এমন রাখুন, যাতে দুবার চললেও ক্ষতি না হয়। ভাষা অনুযায়ী /bn বা /en-এ পাঠানোও একই কৌশল; বাংলা সাইটের locale routing পর্ব ৫৯ (বাংলা সাইট: i18n)-এ।
যা proxy-তে করবেন না
Docs স্পষ্ট বলে: proxy ধীর ডেটা আনার জায়গা নয়, আর পূর্ণ session বা অনুমতি ব্যবস্থাও নয়; এটি optimistic check-এর জন্য। fetch-এর cache option-ও এখানে কাজ করে না। কেন এত জোর? একটি হিসাব দেখুন।
হিসাব: দিনে ২,০০,০০০ page view. প্রতিটিতে ১টি HTML, ১২টি static ফাইল আর ৩টি RSC prefetch, মোট ১৬টি অনুরোধ। ধরুন proxy প্রতিবার database থেকে session পড়ে, গড়ে ২০ ms.
Matcher ছাড়া: 2,00,000 × 16 = ৩২,০০,০০০ বার proxy, database-এর সময় 32,00,000 × 20 ms = 64,000 s ≈ ১৭.৮ ঘণ্টা প্রতিদিন।
Static বাদ দিয়ে matcher: 2,00,000 × 4 = ৮,০০,০০০ বার, 16,000 s ≈ ৪.৪ ঘণ্টা।
Proxy-তে শুধু cookie আছে কি না দেখলে: database-এর কাজ ০। আসল session যাচাই হয় শুধু সেই handler বা পাতায়, যেখানে ডেটা লাগে।
সংখ্যাগুলো অনুমান, কিন্তু অনুপাতটি আসল: proxy সংখ্যায় সবচেয়ে বেশিবার চলে, তাই এর প্রতিটি মিলিসেকেন্ড গুণ হয়। আর অনুমতি কেন শুধু proxy-তে নয়, তার একটি বাস্তব কারণও আছে। ২০২৫ সালের মার্চে Next.js-এ একটি গুরুতর দুর্বলতা প্রকাশ পায় (CVE-2025-29927): একটি internal header-এর অপব্যবহারে কিছু self-hosted অ্যাপে middleware পুরোপুরি এড়িয়ে যাওয়া সম্ভব ছিল। যেসব অ্যাপ অনুমতি শুধু middleware-এ যাচাই করত, তারা খোলা হয়ে পড়েছিল। ঠিক করা version (যেমন 15.2.3 আর 14.2.25) দ্রুত প্রকাশিত হয়। শিক্ষা: proxy একটি স্তর, একমাত্র দেয়াল নয়। Docs-ও বলে, প্রতিটি Server Function-এর ভেতরে আলাদা অনুমতি যাচাই করুন, কারণ matcher বা route বদলালে proxy নিঃশব্দে বাদ পড়তে পারে। অনুমতির নকশা পর্ব ৪৮-এ।
| ভালো কাজ | এড়িয়ে চলুন |
|---|---|
| Cookie আছে কি না দেখে login-এ redirect | প্রতিটি অনুরোধে database-এ session বা role পড়া |
| Request id, নিরাপত্তার header বসানো | অনুমতির একমাত্র যাচাই এখানে রাখা |
| A/B বা locale অনুযায়ী rewrite | বাইরের ধীর API ডাকা |
| পুরোনো URL থেকে নতুনে redirect (জটিল হলে) | সরল redirect, যা next.config-এর redirects-এ হয় |
| অনেক route-এর জন্য এক জায়গায় CORS | বড় header, যা 431 error ঘটাতে পারে |
৯ · কোনটি কখন — route handler, server action, না আলাদা Express API
Next.js-এ server-এ কোড চালানোর তিনটি পথ পাশাপাশি। Server action (পর্ব ৫৪) হলো আপনার নিজের পাতার form আর বোতামের জন্য; Next.js ভেতরে এটিকে সেই পাতার route-এ একটি POST হিসেবে পাঠায়, URL আপনি ঠিক করেন না। Route handler হলো একটি নিজস্ব URL, যা বাইরের যে কেউ জানে আর ডাকে। আর মডিউল ৪-এর মতো আলাদা Express বা অন্য server একটি পুরো স্বাধীন backend.
| কাজ | ভালো পথ | কেন |
|---|---|---|
| নিজের পাতার “কার্টে যোগ” বোতাম | Server action | Type ভাগ হয়, form JavaScript ছাড়াও চলে, revalidate সহজ |
| মোবাইল অ্যাপের জন্য পণ্যের JSON | Route handler | স্থির URL, version করা যায়, অন্য client ডাকতে পারে |
| Payment webhook | Route handler | বাইরের সেবা নির্দিষ্ট URL-এ POST করে, raw body লাগে |
| RSS, CSV export, ফাইল download | Route handler | HTML নয়, নিজস্ব Content-Type |
| WebSocket chat, দীর্ঘ background কাজ | আলাদা server | Route handler এক অনুরোধ-এক উত্তরের জন্য; serverless-এ সময়সীমাও থাকে |
| অনেক দল, অনেক client, আলাদা release | আলাদা Express API | Frontend আর backend আলাদা গতিতে বদলাতে পারে |
আলাদা Express API কখন রাখবেন? (১) Backend-এর ক্রেতা অনেক: web, Android, iOS, partner. তখন API নিজেই একটি পণ্য, Next.js তার একটি ক্রেতা মাত্র। (২) দীর্ঘ সংযোগ বা কাজ: WebSocket (পর্ব ৬০), queue worker (পর্ব ৬২)। (৩) দল আলাদা, release আলাদা। (৪) মডিউল ৪-এর পরিপক্ক middleware-এর বাস্তুতন্ত্র (পর্ব ২৪ থেকে পর্ব ৩১) যদি আপনার কাছে জরুরি হয়। আবার উল্টো দিকে, ছোট দলে একটি Next.js প্রকল্পই পাতা, server action আর কয়েকটি route handler নিয়ে পুরো অ্যাপ চালাতে পারে; deploy একটি, type একটি। মাঝামাঝি একটি পথও প্রচলিত: Next.js-এর route handler পাতলা BFF হিসেবে, যা পেছনের আলাদা API-কে ডাকে। এখানে সম্প্রদায় একমত নয়, আর সঠিক উত্তর দলের আকার, client-এর সংখ্যা আর hosting-এর ওপর নির্ভর করে।
আরও গভীরে: Route handler-এর Request/Response আসে WHATWG Fetch Standard থেকে, তাই একই handler অনেক runtime-এ চলে; WinterCG (এখন Ecma-র TC55, WinterTC নামে) এই সাধারণ server API নিয়ে কাজ করছে। HMAC-এর সংজ্ঞা RFC 2104 (১৯৯৭)। Webhook-এর স্বাক্ষর, সময় আর id-এর একটি সাধারণ ছাঁচের প্রস্তাব “Standard Webhooks” নামে শিল্পের কয়েকটি প্রতিষ্ঠানের উদ্যোগে লেখা হয়েছে; প্রতিটি gateway তা মানে না, তাই নিজের gateway-র docs-ই চূড়ান্ত। Hash আর MAC-এর তত্ত্ব আছে Hash আর MAC পর্বে।
পরীক্ষা ও ইন্টারভিউতে:
- Next.js 16-এ middleware ফাইলের নাম কী?
proxy.ts, export করা ফাংশনproxy; পুরোনোmiddleware.tsdeprecated. - Route handler-এর GET কি ডিফল্টে cache হয়? না, Next.js 15 থেকে dynamic;
force-staticবা Cache Components দিয়ে স্পষ্ট করতে হয়। - Webhook-এ raw body কেন? স্বাক্ষর byte-এর ওপর; parse করে আবার লিখলে byte বদলায়।
- Proxy-তে অনুমতি যাচাই যথেষ্ট কি? না, এটি হালকা প্রাথমিক যাচাই; আসল যাচাই handler বা data layer-এ।
- Native মোবাইল অ্যাপে CORS লাগে কি? না, CORS browser-এর নিয়ম; WebView বা web origin হলে লাগে।
১০ · AI-এর যুগে route handler আর proxy
Next.js-এর API এত দ্রুত বদলেছে যে AI-এর লেখা কোডে পুরোনো ধরন খুব সাধারণ। pages/api-এর req, res, synchronous params.id, middleware.ts, “GET ডিফল্টে static” — সবই কোনো না কোনো সময়ে সঠিক ছিল। AI সেই মিশ্রণ থেকে লেখে। নিচের ভুলগুলো খেয়াল রাখুন।
| AI-এর ভুল | কীভাবে ধরবেন |
|---|---|
export default function handler(req, res) app ফোল্ডারে | App Router-এ method-এর নামে export; next build-এ route table দেখুন |
params.id সরাসরি, await নেই | TypeScript-এ RouteContext দিন, compile-এই ধরা পড়বে |
নতুন প্রকল্পে middleware.ts | Build-এর deprecation সতর্কতা পড়ুন; runtime-ও আলাদা |
Webhook-এ await req.json() তারপর স্বাক্ষর | Space বদলানো body দিয়ে test; raw body চাই |
Signature === দিয়ে তুলনা, সময়সীমা নেই | timingSafeEqual আর t আছে কি না দেখুন |
CORS-এ *, credentials-সহ | Diff-এ Allow-Origin: * খুঁজুন; অচেনা origin দিয়ে preflight চালান |
| Proxy-তে database বা অনুমতির পুরো যুক্তি | Handler থেকে অনুমতি সরিয়ে test করুন, খোলা থাকে কি না |
| নেই এমন package, যেমন নকল “next-webhook-verify” | npm view দিয়ে খুঁজুন; node:crypto-ই যথেষ্ট |
AI-কে version বলে দিন, আর প্রমাণ চান। আসল webhook secret, gateway-র key বা ক্রেতার তথ্য prompt-এ দেবেন না।
AI-এর লেখা route handler আর proxy মেলানোর চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১Next.js “middleware”-এর নাম বদলে “proxy” রাখল কেন? শুধু নাম, না এর পেছনে নকশার দর্শন আছে?
Middleware আসে Next.js 12-এ, ২০২১ সালে। তখনকার চিন্তা ছিল “edge”: CDN-এর মতো ব্যবহারকারীর কাছাকাছি ছড়ানো server-এ খুব ছোট কোড চালানো, যাতে redirect বা A/B-এর সিদ্ধান্ত মূল server পর্যন্ত না গিয়েই হয়। তাই এটি চলত Edge runtime-এ, যেখানে শুধু Web API, Node.js-এর fs বা অনেক database driver চলে না। সীমাবদ্ধতাটি ইচ্ছাকৃত ছিল: ছোট আর দ্রুত থাকো।
বাস্তবে ডেভেলপাররা Express-এর অভিজ্ঞতা নিয়ে এলেন। Express-এ middleware মানে route-এর ভেতরের স্তর: body parse, session পড়া, অনুমতি, log. নাম এক হওয়ায় অনেকে Next.js middleware-কেও সেভাবে ব্যবহার শুরু করলেন: প্রতিটি অনুরোধে session database, পুরো অনুমতি ব্যবস্থা। Edge-এর সীমায় আটকে গিয়ে চাপ এলো Node.js runtime-এর, যা ১৫.২-এ পরীক্ষামূলক আর ১৫.৫-এ stable হয়।
২০২৫ সালের CVE-2025-29927 এই ভুল ব্যবহারের ঝুঁকি স্পষ্ট করে। একটি দুর্বলতায় middleware এড়ানো গেলে যেসব অ্যাপ অনুমতি শুধু সেখানে রেখেছিল, তারা খোলা হয়ে যায়। Next.js 16-এ নাম বদলের ব্যাখ্যায় docs বলে, “middleware” শব্দ Express-এর সাথে গুলিয়ে যায়, আর এই ফিচার শেষ উপায় হিসেবে থাকা উচিত। “Proxy” শব্দটি বোঝায় অ্যাপের সামনের একটি network সীমানা, যা অ্যাপের মূল runtime থেকে আলাদা জায়গাতেও চলতে পারে।
মজার ব্যাপার, নাম বদলের সাথে runtime-এর ডিফল্টও বদলেছে। আমাদের ল্যাবে proxy.ts Node.js-এ চলেছে, পুরোনো নামের একই ফাইল Edge-এ। অর্থাৎ ক্ষমতা বেড়েছে, আর নাম দিয়ে বলা হচ্ছে, “ক্ষমতা আছে বলেই সব এখানে কোরো না”। Software নকশায় এটি একটি পরিচিত কৌশল: নাম ব্যবহারকারীর মানসিক মডেল গড়ে, আর ভুল নাম ভুল ব্যবহার ডেকে আনে।
মূল উপলব্ধি: একটি API-র নাম তার নিয়মের অংশ। Proxy মানে দরজার পাহারা: দ্রুত পথ দেখানো, header বসানো, সন্দেহ হলে login-এ পাঠানো। ঘরের ভেতরের তালা, অর্থাৎ আসল অনুমতি, থাকবে handler আর data layer-এ।
প্র ০২Webhook-এর নিরাপত্তায় HMAC স্বাক্ষর কেন? URL-এ গোপন token বা gateway-র IP-র তালিকা দিয়ে কি একই কাজ হয় না?
তিনটি পথই বাস্তবে দেখা যায়, আর প্রতিটি আলাদা প্রশ্নের উত্তর দেয়। URL-এ গোপন token (/webhook?key=…) বলে “পাঠানো ব্যক্তি key জানেন”। কিন্তু URL log-এ, proxy-র ইতিহাসে, monitoring টুলে ছড়িয়ে পড়ে। একবার ফাঁস হলে যে কেউ যেকোনো body পাঠাতে পারেন। আর এটি body-র অখণ্ডতা নিয়ে কিছুই বলে না।
IP-র তালিকা বলে “অনুরোধ এই ঠিকানা থেকে এসেছে”। Cloud-এর যুগে gateway-র ঠিকানা বদলায়, আর আপনার সামনে load balancer বা CDN থাকলে আসল ঠিকানা বের করাও ভুলের উৎস (X-Forwarded-For বিশ্বাস করার প্রশ্ন)। এটি একটি ভালো অতিরিক্ত স্তর হতে পারে, একমাত্র দেয়াল নয়।
HMAC-এর সংজ্ঞা RFC 2104-এ, ১৯৯৭ সালে, Krawczyk, Bellare আর Canetti-র লেখায়। এটি বলে “এই নির্দিষ্ট byte-গুলো এমন কেউ বানিয়েছেন, যিনি key জানেন”। Key কখনো network-এ যায় না, শুধু স্বাক্ষর যায়। Body-র একটি অক্ষর বদলালেও স্বাক্ষর মেলে না, যা আমাদের run-এ দেখা গেছে: শুধু space বদলানোতেও 401.
তবু HMAC একা যথেষ্ট নয়। একটি বৈধ বার্তা হুবহু আবার পাঠালে স্বাক্ষর ঠিকই থাকে। তাই স্বাক্ষরের ভেতরে সময় (t), আর একটি সীমা। সীমার ভেতরের replay ঠেকায় event id-এর idempotency. তুলনায় সময়ের পার্থক্য ঠেকাতে timingSafeEqual. আর key বদলানোর (rotation) সময় অনেক gateway একসাথে দুটি স্বাক্ষর পাঠায়, তাই কোডে একাধিক v1 মেলানোর জায়গা রাখা ভালো। কিছু gateway অসমমিতিক স্বাক্ষর (public key) ব্যবহার করে, যাতে দোকানের কাছে শুধু যাচাইয়ের key থাকে, বানানোর নয়।
মূল উপলব্ধি: নিরাপত্তার প্রতিটি ব্যবস্থা একটি নির্দিষ্ট প্রশ্নের উত্তর: কে পাঠাল, কী পাঠাল, কখন পাঠাল, কয়বার। HMAC প্রথম দুটি, সময়সীমা তৃতীয়টি, idempotency চতুর্থটি। চারটি মিলেই একটি নিরাপদ webhook.
প্র ০৩Next.js 13-এ GET handler ডিফল্টে static ছিল, 15-এ dynamic হলো। এই উল্টো যাত্রা থেকে framework নকশার কী শেখার আছে?
App Router যখন আসে (Next.js 13, ২০২২–২৩), দর্শন ছিল “যত পারা যায় static, ডিফল্টে cache”. Static মানে দ্রুত, সস্তা, CDN-এ রাখা যায়। GET handler-ও এই নীতিতে চলত: request না পড়লে build-এর সময় একবার চলে স্থির উত্তর। fetch-ও ডিফল্টে cache হতো।
সমস্যা হলো, ডেভেলপাররা এটি আশা করেননি। Express বা যেকোনো backend-এর অভিজ্ঞতায় একটি API মানে প্রতি অনুরোধে চলা কোড। “আমার API সময় দেখায়, কিন্তু সময় বদলায় না” বা “database বদলেছে, API পুরোনো ডেটা দিচ্ছে”, এমন অভিযোগ সম্প্রদায়ের আলোচনায় বারবার এসেছে। ভুলটি চোখে পড়ত production-এ, development-এ নয়, কারণ next dev প্রতিবার চালায়। এটি সবচেয়ে খারাপ ধরনের অবাক হওয়া।
Next.js 15-এ (২০২৪) দল ডিফল্ট উল্টে দেয়: GET handler আর fetch ডিফল্টে cache নয়। Cache চাইলে স্পষ্ট বলতে হবে। Next.js 16-এর Cache Components আরও এক ধাপ এগিয়ে: ডিফল্টে অনুরোধের সময়, আর 'use cache' দিয়ে কোন অংশ cache তা কোডেই লেখা। আমাদের run-এ দেখা গেল, এই মোডে কিছুই না পড়া /api/info নিজে থেকেই prerender হয়, কারণ তাতে ভুল হওয়ার সুযোগ নেই।
শিক্ষাটি পুরোনো: “principle of least astonishment”, অর্থাৎ ব্যবহারকারী যা আশা করেন, ডিফল্ট তা-ই হোক। Performance-এর জন্য cache জরুরি, কিন্তু ভুল ডেটা দেওয়া cache-এর চেয়ে ধীর সঠিক উত্তর ভালো। আর ডিফল্ট বদলানো framework-এর জন্য খরচের: হাজার tutorial, Stack Overflow-এর উত্তর আর AI-এর প্রশিক্ষণের ডেটা পুরোনো আচরণ শেখাতে থাকে।
মূল উপলব্ধি: Cache একটি সচেতন সিদ্ধান্ত, লুকানো ডিফল্ট নয়। আর যেকোনো framework-এর আচরণ নিয়ে সন্দেহ হলে, build-এর আউটপুট আর দুবার curl, এই দুটিই সবচেয়ে বিশ্বস্ত শিক্ষক।
প্র ০৪Payment webhook “একবারই” প্রক্রিয়া করা কঠিন কেন? Gateway তো একবারই পাঠানোর কথা।
Distributed system-এর একটি মৌলিক সত্য: network-এ “ঠিক একবার পৌঁছানো” নিশ্চিত করা যায় না। পাঠানো বার্তা হারাতে পারে, অথবা পৌঁছানোর পর উত্তরটি হারাতে পারে। পাঠানো পক্ষ দুটির পার্থক্য জানে না। তাই নিরাপদ পথ একটিই: উত্তর না পেলে আবার পাঠানো। একে বলে “at-least-once” delivery, আর বেশিরভাগ webhook ব্যবস্থা এই নীতিতে চলে বলে নিজেদের docs-এ জানায়।
বাজারঘরের দিক থেকে ভাবুন। Webhook এলো, আমরা order paid করলাম, ক্রেতাকে SMS পাঠালাম, তারপর উত্তর দিতে গিয়ে server-এর সময়সীমা পার হলো। Gateway 200 পায়নি, তাই আবার পাঠাল। Idempotency না থাকলে দ্বিতীয় SMS, দ্বিতীয় invoice, এমনকি stock দুবার কমে যেতে পারে। এর উল্টোটিও আছে: একই অর্ডারে “ব্যর্থ” আর “সফল” ঘটনা উল্টো ক্রমে আসতে পারে।
সমাধানের তিনটি স্তর। এক, event id-তে database-এর unique constraint, আর order-এর পরিবর্তন একই transaction-এ। দুটি অনুরোধ একসাথে এলে একটি জিতবে, অন্যটি constraint-এ আটকাবে। Memory-র Set যথেষ্ট নয়, কারণ server-এর একাধিক কপি থাকে। দুই, অবস্থার যন্ত্র (state machine): “paid” order আবার “pending” হবে না, যে ক্রমেই ঘটনা আসুক। তিন, দ্রুত 200, ভারী কাজ queue-তে। এতে retry-র কারণই কমে।
পর্ব ৩৩-এ idempotency key দিত client, এখানে দিচ্ছে gateway, নীতি একই। আর একটি বাস্তব অভ্যাস: যেসব ঘটনা যাচাইয়ে ব্যর্থ হলো, সেগুলোর log রাখুন (secret ছাড়া)। হঠাৎ অনেক ব্যর্থতা মানে হয় key বদলেছে, নয়তো কেউ চেষ্টা করছেন।
মূল উপলব্ধি: “একবারই পাঠানো” নয়, “একবারই প্রভাব”, এটিই লক্ষ্য। পাঠানো অনেকবার হতে পারে ধরে নিয়ে নকশা করুন, আর database-কে শেষ বিচারক বানান।
প্র ০৫Next.js-এ route handler থাকলে আলাদা Express backend কি অপ্রয়োজনীয়? সম্প্রদায় এখানে দুই ভাগ কেন?
এক পক্ষের যুক্তি সরলতা। একটি repository, একটি deploy, TypeScript-এর type পাতা থেকে database পর্যন্ত এক সুতোয়। Server action আর route handler মিলে ছোট-মাঝারি অ্যাপের প্রায় সব কাজ হয়ে যায়। দুই আলাদা server-এর মধ্যে CORS, auth-এর token ভাগ, version মেলানো, এসব ঝামেলাও থাকে না। ছোট দলের জন্য এটি বড় সুবিধা।
অন্য পক্ষের যুক্তি আলাদা দায়িত্ব। Backend যখন অনেক client-এর জন্য, যেমন web, Android, iOS আর partner-দের API, তখন সেটিকে একটি frontend framework-এর ভেতরে রাখা অস্বস্তিকর। Next.js-এর release-এর গতি দ্রুত, আর এই পর্বেই দেখলেন কতকিছু বদলেছে: params async, cache-এর ডিফল্ট, middleware-এর নাম আর runtime. যে API বছরের পর বছর স্থির থাকা দরকার, তাকে এই বদলের সাথে বাঁধতে অনেকে চান না।
প্রযুক্তিগত সীমাও আছে। Route handler এক অনুরোধ, এক উত্তরের মডেলে চলে। Serverless hosting-এ সাধারণত সময়সীমা থাকে, আর WebSocket-এর মতো দীর্ঘ সংযোগ বা queue worker চালানো কঠিন। Self-hosted next start-এ এসব কিছুটা সহজ, কিন্তু তখন Next.js-এর অনেক hosting-সুবিধা কাজে আসে না। এর বিপরীতে Express-এর middleware-এর বিশাল ভান্ডার আর পরিচিত মডেল (মডিউল ৪)।
বাস্তবে অনেক দল মাঝামাঝি পথ নেন। Next.js-এর route handler আর server action একটি পাতলা Backend-for-Frontend: শুধু পাতার যা লাগে। পেছনে একটি আলাদা API service, যা ব্যবসার নিয়ম আর database-এর মালিক। Sam Newman-এর মতো লেখকেরা এই BFF ধরনের পক্ষে-বিপক্ষে বিস্তারিত লিখেছেন। প্রশ্নটা তাই “কোনটি ভালো” নয়, “কতজন client, কত বড় দল, কী ধরনের hosting”.
মূল উপলব্ধি: Route handler আছে বলেই backend Next.js-এ রাখতে হবে, এমন নয়; আবার আলাদা backend সবসময় পরিপক্কতার চিহ্নও নয়। সিদ্ধান্তটি client, দল আর বদলের গতির হিসাব।
অনুশীলন
হিসাব, কমান্ড, সমালোচনা আর আউটপুট অনুমান, চারটিই আছে।
-
Proxy-র খরচের হিসাব: একটি সাইটে দিনে ৫০,০০০ page view. প্রতি view-এ ১টি HTML, ২০টি static ফাইল আর ২টি RSC prefetch. Proxy প্রতিবার database থেকে session পড়ে, গড়ে ১৫ ms. (ক) Matcher ছাড়া দিনে কতবার proxy চলবে, database-এ মোট কত সময়? (খ) Static বাদ দিলে? (গ) কোন নকশায় database-এর এই খরচ শূন্য হয়?
(ক) প্রতি view-এ 1 + 20 + 2 = ২৩টি অনুরোধ। 50,000 × 23 = ১১,৫০,০০০ বার। সময় 11,50,000 × 15 ms = 17,250 s ≈ ৪.৮ ঘণ্টা। (খ) 50,000 × 3 = ১,৫০,০০০ বার, 2,250 s = ৩৭.৫ মিনিট, অর্থাৎ প্রায় ৮৭% কম। (গ) Proxy-তে শুধু cookie আছে কি না দেখা (optimistic check), database নয়; আসল session যাচাই শুধু সেই handler বা পাতায়, যেখানে ব্যবহারকারীর ডেটা লাগে।
-
CORS যাচাই (কমান্ড):
/api/mobile/products-এর জন্য দুটি curl কমান্ড লিখুন, যা browser-এর preflight-এর মতো অনুরোধ পাঠায়: একটি অনুমোদিত originhttps://m.bazarghar.testথেকে, একটি অচেনাhttps://other.exampleথেকে। উত্তরে কোন header দেখে পাস-ফেল ঠিক করবেন?curl -si -X OPTIONS URL -H 'Origin: https://m.bazarghar.test' -H 'Access-Control-Request-Method: GET', তারপর একই কমান্ড অন্য origin দিয়ে। পাস: প্রথমটিতেaccess-control-allow-origin: https://m.bazarghar.test(হুবহু,*নয়), দ্বিতীয়টিতে কোনোaccess-control-allow-originনেই। সাথেVary: Originআছে কি না দেখুন। মনে রাখুন, দ্বিতীয়টিতেও status204আসতে পারে; সিদ্ধান্ত header দেখে, status দেখে নয়। -
কোড সমালোচনা (নকশা): অন্তত পাঁচটি সমস্যা ধরুন:
// app/api/pay-callback/route.ts export async function GET(req) { const { orderId, status, amount } = Object.fromEntries(new URL(req.url).searchParams); if (status === 'success') await db.order.update({ where: { id: orderId }, data: { paid: true } }); return Response.json({ ok: true }, { headers: { 'Access-Control-Allow-Origin': '*' } }); }- কোনো স্বাক্ষর যাচাই নেই: যে কেউ URL-এ
status=successলিখে order paid করতে পারেন। GET-এ অবস্থা বদলানো: GET নিরাপদ হওয়ার কথা; link preview বা prefetch-ও এটি ডাকতে পারে। Webhook হবেPOST, raw body-সহ।- Amount-এর সাথে order-এর আসল দাম মেলানো হয়নি।
- Idempotency নেই: একই callback দুবার এলে পরের কাজগুলো (SMS, stock) দুবার হতে পারে।
- Zod বা কোনো যাচাই নেই:
orderIdযেকোনো কিছু হতে পারে। Access-Control-Allow-Origin: *অপ্রয়োজনীয়: gateway browser নয়, CORS এখানে অর্থহীন, শুধু ভুল বার্তা দেয়।
- কোনো স্বাক্ষর যাচাই নেই: যে কেউ URL-এ
-
আউটপুট অনুমান: একটি
proxy.ts-এর matcher'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|css)$).*)', আর এটিpathname.startsWith('/admin')হলে cookiesidছাড়া/login-এ redirect করে। Cookie ছাড়া এই চারটি অনুরোধের প্রতিটিতে কী ঘটবে? (ক)/admin/orders(খ)/admin.css(গ)/administrator-guide(ঘ)/_next/static/app.js(ক) Proxy চলে, prefix মেলে,
307→/login. (খ) শেষে.css, matcher বাদ দেয়, proxy চলেই না, ফাইল থাকলে সরাসরি দেওয়া হয়। (গ) Proxy চলে, আর"/administrator-guide".startsWith("/admin")সত্য, তাই এটিও307, যদিও সম্ভবত খোলা পাতা। এটিই prefix-এর বাগ; ঠিক নিয়ম=== '/admin' || startsWith('/admin/'). (ঘ)_next/static, matcher বাদ দেয়, proxy চলে না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমNext.js-এ একটি অনুরোধ আসার পর যে ক্রমে ধাপগুলো দেখা হয়, সাজান (▲▼ চেপে সরান)
next.config-এরheadersnext.config-এরredirects- Proxy (
proxy.ts) beforeFilesrewrite- Filesystem route:
public/,_next/static,app/ - Dynamic route, যেমন
/products/[id]
Next.js 16-এ একটি নতুন প্রকল্পে route মেলার আগে redirect আর header বসানোর ফাইলের প্রস্তাবিত নাম কী?
middleware.tsroute.tsproxy.tsgateway.ts
Next.js 16, খালি config, অর্থাৎ cacheComponents বন্ধ। app/api/clock/route.ts-এ শুধু export async function GET() { return Response.json({ at: new Date().toISOString() }); }. next build আর next start-এর পর দুটি অনুরোধে কী পাওয়া যাবে?
- দুবারই build-এর সময়ের একই মান
- প্রতিবার আলাদা, অনুরোধের সময়ের মান
- Build error, কারণ new Date() নিষিদ্ধ
405 Method Not Allowed
একটি payment webhook-এর HMAC যাচাই করার আগে body কোন পদ্ধতিতে পড়া উচিত?
await request.text(), তারপর যাচাই, তারপর parseawait request.json(), তারপরJSON.stringifyকরে যাচাইawait request.formData()- আগে
json(), পরে আবারtext()
Proxy-র matcher '/((?!api|_next/static|_next/image|favicon.ico).*)'. কোন অনুরোধে proxy চলবে?
/api/products/_next/static/chunks/a.js/favicon.ico/account/orders
Kotlin-এ লেখা একটি native Android অ্যাপ একটি Next.js route handler থেকে JSON নেয়। Route handler-এ কোনো CORS header নেই। কী ঘটবে?
- অ্যাপ CORS error পাবে, উত্তর পড়তে পারবে না
- Server অনুরোধটি 403 দিয়ে ফিরিয়ে দেবে
- অ্যাপ স্বাভাবিকভাবে উত্তর পাবে
- আগে একটি OPTIONS preflight যাবে, তারপর ব্যর্থ হবে
একটি Next.js অ্যাপে admin পাতার অনুমতি শুধু proxy-তে যাচাই হয়। নিরাপত্তার দিক থেকে সবচেয়ে ভালো পরামর্শ কোনটি?
- Proxy-তে database থেকে role পড়ে যাচাই আরও কড়া করা
- Proxy-তে হালকা redirect রেখে আসল অনুমতি handler, server action আর data layer-এ যাচাই করা
- Matcher-এ সব path যোগ করা, যাতে কিছু বাদ না পড়ে
- Admin route-এ CORS বন্ধ রাখা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
route.ts-এ method-এর নামে ফাংশন, WebRequestঢোকে,Responseবেরোয়;paramsআরcookies()/headers()async.- যাচাই Zod দিয়ে, ভুল problem details-এ: 201, 400, 401, 404, 405, 422 ঠিক জায়গায়।
- Next.js 15+ এ GET handler ডিফল্টে cache হয় না;
force-staticবা Cache Components, আর route table-এ○/ƒদেখে নিশ্চিত হোন। - Webhook: raw body → HMAC + সময়সীমা → parse → event id-তে একবারই; দাম নিজের order-এর সাথে মেলান।
proxy.ts(আগে middleware), Node.js runtime; matcher-এ static বাদ, শুধু হালকা redirect, header, rewrite; আসল অনুমতি handler-এ।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৮ · SEO — metadata, sitemap, structured data আর Open Graphপরবর্তী পর্বNext.js-এ খুঁজে পাওয়ার মতো পাতা।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ।
- পর্ব ৩৩ · Status code আর error formatপেছনে ফিরেProblem details আর idempotency-র পূর্ণ আলোচনা।
- Cybersecurity · Hash আর MACগভীরে যানHMAC কেন কাজ করে।
- Cybersecurity · API securityপাশের ট্র্যাকAPI-র ঝুঁকি আর প্রতিরক্ষা।