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

Request যাচাই — Zod দিয়ে schema আর নিরাপদ ইনপুট

Request Validation — Zod Schemas and Safe Input in Express

Taught in Bangla, this lesson treats every request as an untrusted letter, builds a validate(schema) middleware for Express 5 that stores clean data in res.locals, explains why params and query are strings and the traps of z.coerce, strips unknown keys to stop mass assignment, sets limits on body size, arrays and strings, uses one error shape for 400, 413, 415 and 422, separates shape from business rules, and generates JSON Schema and OpenAPI.

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

  • Request-এর কোন চারটি অংশ যাচাই করবেন, আর কেন TypeScript একা যথেষ্ট নয়
  • Express 5 আর Zod 4 দিয়ে পুনর্ব্যবহারযোগ্য validate middleware বানানো
  • z.coerce, pagination-এর সীমা, অজানা key, আকারের সীমা আর এক ছাঁচের error
  • Schema থেকে type, আর আকৃতি বনাম ব্যবসার নিয়মের সীমারেখা
  • Sanitize, escape আর parameterized query-র আসল ভাগ, সঙ্গে একটি জমা review-এর HTML

১ · Server-এ পৌঁছানো কোনো কিছুই বিশ্বাস করবেন না

ডাকঘরের বাছাই টেবিলে একটি খাম এলো। খামের ওপরে লেখা “জরুরি, সরকারি”। বাছাইকর্মী কি সেই কথা মেনে নেন? না। তিনি ঠিকানা, ডাকটিকিট আর ওজন নিজে দেখেন, কারণ খামের লেখা যে কেউ লিখতে পারে। আপনার server-ও ঠিক এই টেবিল। Request যে-ই পাঠাক, প্রতিটি অক্ষর অচেনা মানুষের লেখা ধরে নিতে হয়।

পর্ব ২১-এ (বড় form — React Hook Form, Zod আর দুই দিকের validation) আপনি Zod schema, safeParse আর browser-server-এ ভাগ করা নিয়ম দেখেছেন। সেখানে মূল কথা ছিল, browser-এর যাচাই সুবিধা, server-এর যাচাই নিরাপত্তা। সেই সিদ্ধান্ত এখানে আবার প্রমাণ করব না। এই পর্ব শুধু পরের প্রশ্নটির উত্তর দেবে: Express server-এ এটি পরিষ্কারভাবে, প্রতিটি route-এ, ভুল না করে কীভাবে লিখবেন?

ভেতরের জগৎ আর বাইরের জগতের মাঝের দাগটিকে বলে trust boundary. দাগের বাইরের সব ডেটা সন্দেহজনক। দাগ পেরোনোর মুহূর্তে একবার ভালো করে যাচাই করলে ভেতরের কোড নিশ্চিন্তে চলে। আর সেই কাজটি করতে হয় চারটি জায়গায়:

অংশউদাহরণযা আসেসাধারণ বিপদ
params (route params)/products/2সব সময় stringabc, -1, 2.5
query (query string)?page=2&limit=20string বা string-এর arraylimit=1000000
body (HTTP body)JSON orderযেকোনো JSON, অথবা undefinedভুল type, বাড়তি key, বিশাল array
headers (HTTP header)Idempotency-Keyনাম ছোট হাতের, মান stringনিজের বানানো header-এ ভুল ধরন

এখানে একটি ভুল ধারণা ভাঙা দরকার। অনেকে ভাবেন, handler-এ req.body as Order লিখলেই ডেটা Order হয়ে গেল। তা হয় না। পর্ব ০৬-এ দেখেছেন, TypeScript-এর type compile-এর পরে মুছে যায় (type erasure). as দিয়ে আপনি শুধু compiler-কে একটি প্রতিশ্রুতি দিলেন, আর বাইরের লোক সেই প্রতিশ্রুতি জানেন না। চলার সময়ে পাহারা দিতে পারে কেবল চলমান কোড, অর্থাৎ schema.

মূল ধারণা

যাচাইয়ের লক্ষ্য শুধু “ভুল আটকানো” নয়। লক্ষ্য হলো সীমানায় একবার অচেনা ডেটা থেকে চেনা ডেটা তৈরি করা, যাতে handler, service আর repository-তে বারবার typeof লিখতে না হয়।

পুরো পথে কোন ফটক কী আটকায়, নিচের ছবিতে দেখে নিন। পরের অংশগুলো এই ছবির এক একটি ফটক।

একটি request-এর পথে ফটকগুলো

Each gate rejects a different kind of problem

Express request-এর ফটক: size, parse, validate, handler, service, repository, response express.json আকার আর JSON validate(schema) আকৃতি, type, সীমা Handler শুধু যাচাই করা ডেটা Service মজুত, দাম, ভূমিকা Repository parameterized query Response HTML-এ escape 400 413 422 415 409
ছবি ২৭.১ — প্রতিটি ফটক নিজের ধরনের সমস্যা আটকায়। কোনো একটি ফটক অন্যটির কাজ করে দেয় না।

২ · একটি validate(schema) middleware

প্রতিটি handler-এর শুরুতে safeParse আর if লিখলে কাজ হয়, কিন্তু ৪০টি route-এ ৪০ বার একই কোড জমে। কোথাও একবার ভুলে গেলেই ফাঁক। হোটেলের রিসেপশনের কথা ভাবুন: প্রতিটি কক্ষের দরজায় আলাদা করে পরিচয়পত্র দেখা হয় না, এক জায়গায় দেখা হয়। Middleware সেই রিসেপশন। পর্ব ২৬-এ (Middleware) আপনি middleware-এর পাইপলাইন দেখেছেন, এখানে সেই পাইপলাইনে একটি ফটক বসছে।

আগে ভাবনাটি হাতে-কলমে দেখুন। Live বাক্সে npm package আসে না, তাই এখানে Zod নেই। orderRules একটি ছোট শেখার মডেল, আর node:http আসল। chain লাইনটি লক্ষ করুন: Express ভেতরে এমন একটি তালিকাই চালায়।

const http = require('node:http');
const orderRules = {
  phone: v => (typeof v === 'string' && /^01[3-9]\d{8}$/.test(v)) || 'phone: 11 digits',
  qty: v => (Number.isInteger(v) && v >= 1 && v <= 10) || 'qty: 1 to 10',
};
function safeParse(rules, input) {
  const box = Object(input), data = {}, errors = [];
  for (const key in rules) {
    const r = rules[key](box[key]);
    if (r === true) data[key] = box[key]; else errors.push({ path: key, message: r });
  }
  return errors.length ? { success: false, errors } : { success: true, data };
}
const send = (res, code, body) => res.writeHead(code, { Connection: 'close' }).end(JSON.stringify(body));
const validate = (rules) => (req, res, next) => {
  const r = safeParse(rules, req.body);
  if (!r.success) return send(res, 422, { code: 'VALIDATION_FAILED', errors: r.errors });
  req.input = r.data; next();               // শুধু যাচাই করা ডেটা সামনে যায়
};
const createOrder = (req, res) => send(res, 201, { saved: req.input, total: 140 * req.input.qty });
const chain = [validate(orderRules), createOrder];
const server = http.createServer(async (req, res) => {
  let raw = '', i = 0;
  for await (const part of req) raw += part;
  req.body = JSON.parse(raw);
  const next = () => chain[i++](req, res, next); next();
});
server.listen(0, '127.0.0.1', async () => {
  const url = `http://127.0.0.1:${server.address().port}/api/orders`;
  for (const body of [{ phone: '0171', qty: 0 }, { phone: '01712345678', qty: 2 }]) {
    const r = await fetch(url, { method: 'POST', body: JSON.stringify(body) });
    console.log(r.status, await r.text());
  }
  server.close();
});

প্রথম লাইন 422 {"code":"VALIDATION_FAILED","errors":[{"path":"phone","message":"phone: 11 digits"},{"path":"qty","message":"qty: 1 to 10"}]}. দুটি ভুলই একসাথে এলো, আর createOrder একবারও চলেনি, কারণ next() ডাকা হয়নি। দ্বিতীয় লাইন 201 {"saved":{"phone":"01712345678","qty":2},"total":280}. Handler-এর কোডে কোনো যাচাই নেই, তবু সে ১৪০ × ২ = ২৮০ নিশ্চিন্তে হিসাব করল।

আসল রূপ: Express 5 আর Zod 4

এবার সত্যিকারের project. লেখার সময়ে Express 5.3.0, Zod 4.6.7, TypeScript 7.0.2 আর Node 24 ব্যবহার করা হয়েছে। Project-এর গঠন আর চালানোর রীতি পর্ব ২৫-এর (Express) মতোই। এই ফাইলটি পুরো কোর্সে বারবার কাজে লাগবে:

import type { RequestHandler } from 'express';
import { z } from 'zod';

type Part = 'params' | 'query' | 'headers' | 'body';
type Schemas = Partial<Record<Part, z.ZodType>>;
type Input<S extends Schemas> = { [K in keyof S]: z.output<S[K] & z.ZodType> };

export function validate<S extends Schemas>(
  schemas: S,
): RequestHandler<any, any, any, any, { input: Input<S> }> {
  const parts = Object.keys(schemas) as Part[];
  return (req, res, next) => {
    if (schemas.body && !req.is('application/json')) {
      return res.status(415).json({ code: 'JSON_REQUIRED', message: 'send application/json' });
    }
    const input: Record<string, unknown> = {};
    const errors: { in: Part; path: string; message: string }[] = [];

    for (const part of parts) {
      const result = schemas[part]!.safeParse(req[part]);
      if (result.success) input[part] = result.data;
      else for (const issue of result.error.issues) {
        errors.push({ in: part, path: issue.path.join('.'), message: issue.message });
      }
    }

    if (errors.length) {
      return res.status(422).json({ code: 'VALIDATION_FAILED', errors });
    }
    res.locals.input = input as Input<S>;
    next();
  };
}

তিনটি সিদ্ধান্ত লক্ষ করুন। এক, চারটি অংশ একসাথে যাচাই হয়, তাই ক্লায়েন্ট একবারেই সব ভুল পান। দুই, in ঘরটি বলে ভুলটি কোন অংশের। তিন, ফল যায় res.locals-এ, req.query-তে নয়। শেষ সিদ্ধান্তের পেছনে একটি আসল crash আছে। অনেক পুরোনো টিউটোরিয়ালে, আর AI-এর কোডে, দেখবেন req.query = parsed. Express 5-এ:

সত্যিকারের run · Express 5.3.0, ES module · ~/bazarghar

{"thrown":"Cannot set property query of #<IncomingMessage> which has only a getter"}

Express 5-এ req.query একটি getter, লেখার পথ নেই। তাই মূল request-কে অক্ষত রেখে যাচাই করা ফল আলাদা জায়গায় রাখাই পরিষ্কার পথ। এখন route-গুলো ছোট হয়ে গেল, আর schema আলাদা ফাইলে:

import { z } from 'zod';

export const productParams = z.object({
  id: z.coerce.number().int().positive(),
});

export const listQuery = z.object({
  page: z.coerce.number().int().min(1).max(1000).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sort: z.enum(['price', 'name', 'new']).default('new'),
  q: z.string().trim().max(50).optional(),
});

export const orderBody = z.object({
  phone: z.string().regex(/^01[3-9]\d{8}$/, '১১ সংখ্যার মোবাইল নম্বর দিন'),
  items: z.array(z.object({
    sku: z.string().min(1).max(40),
    qty: z.number().int().min(1).max(10, 'একবারে সর্বোচ্চ ১০টি'),
  })).min(1).max(20, 'সর্বোচ্চ ২০টি লাইন'),
  note: z.string().trim().max(200).optional(),
});

export const idemHeaders = z.object({
  'idempotency-key': z.uuid('Idempotency-Key একটি UUID হতে হবে').optional(),
});
app.use(express.json({ limit: '100kb' }));

app.get('/api/products', validate({ query: listQuery }), (req, res) => {
  const { page, limit, sort, q } = res.locals.input.query;
  res.json({ page, limit, sort, q, offset: (page - 1) * limit });
});

app.post('/api/orders', validate({ headers: idemHeaders, body: orderBody }),
  (req, res) => { /* … */ });

একটি ভালো আর একটি ভুল request পাঠিয়ে দেখা যাক। ভুল bad.json-এ ফোন ছোট, একটি qty ০, একটি qty string "2", আর note একটি সংখ্যা। good.json-এ ডাল ২টি (১৪০ × ২) আর চাল ১টি (৪২০).

সত্যিকারের run · 127.0.0.1 · কিছু header ছাঁটা

$ curl -si -X POST -H 'content-type: application/json' \
    --data-binary @good.json http://127.0.0.1:5827/api/orders
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{"orderId":"BG-1001","total":700}

$ curl -si -X POST -H 'content-type: application/json' \
    --data-binary @bad.json http://127.0.0.1:5827/api/orders
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json; charset=utf-8

{"code":"VALIDATION_FAILED","errors":[
{"in":"body","path":"phone","message":"১১ সংখ্যার মোবাইল নম্বর দিন"},
{"in":"body","path":"items.0.qty","message":"Too small: expected number to be >=1"},
{"in":"body","path":"items.1.qty","message":"Invalid input: expected number, received string"},
{"in":"body","path":"note","message":"Invalid input: expected string, received number"}]}

(পড়ার সুবিধায় ভুলের তালিকায় লাইন ভাঙা হয়েছে।) items.1.qty লক্ষ করুন। Body-র "2" সংখ্যায় বদলায়নি, কারণ JSON নিজেই সংখ্যা পাঠাতে পারে, তাই body-তে z.number() কড়া থাকে। Coerce শুধু URL-এর জন্য, পরের অংশের বিষয়।

ইংরেজি বার্তাগুলো Zod-এর ডিফল্ট। Field ধরে নিজের বার্তা দিতে পারেন, যেমন ফোনে দেওয়া হয়েছে। Zod 4-এ একটি বাংলা locale-ও আছে: z.config(z.locales.bn()) দিলে সত্যিকারের run-এ এলো অনেক বড়: number <=100 হতে হবে. ক্রেতার চোখে পড়ে এমন ঘরে তবু নিজের লেখা বার্তাই ভালো।

এখন একটি request-কে ধীরে ধীরে অনুসরণ করা যাক। URL /api/products?page=2&limit=10&sort=price, আর শেষে একটি বাড়াবাড়ি request. প্রতিটি ধাপে মানের type-এ চোখ রাখুন।

1app.get('/api/products',2  validate({ query: listQuery }),3  (req, res) => {4    const { page, limit, sort } = res.locals.input.query;5    const offset = (page - 1) * limit;6    res.json({ page, limit, sort, offset });7});
এই মুহূর্তের মান
req.query.limit
input.page
input.limit
offset
  1. Route মিলল। req.query-তে এখন { page: '2', limit: '10', sort: 'price' }. সব মান string. Handler এখনো চলেনি।
  2. Schema-র প্রথম ঘর। validate-এর ভেতরে safeParse চলল। '2' Number দিয়ে 2 হলো, পূর্ণসংখ্যা, আর ১ থেকে ১০০০-এর মধ্যে।
  3. বাকি ঘর। '10' হলো 10, যা ১০০-এর নিচে। sort তালিকায় আছে। সফল, তাই ফল গেল res.locals.input.query-তে, আর next() ডাকা হলো।
  4. Handler চলল। সে পড়ছে যাচাই করা ফল, req.query নয়। এখানে page সত্যিই একটি সংখ্যা, TypeScript-ও তা জানে।
  5. হিসাব। (২ − ১) × ১০ = ১০. মান string থাকলে '2' - 1 কাজ করে যেত, কিন্তু page + 1 দিত '21'. এই নীরব ভুলগুলোই যাচাই সরিয়ে দেয়।
  6. উত্তর। সত্যিকারের run-এ এলো {"page":2,"limit":10,"sort":"price","offset":10,…}, status 200.
  7. নতুন request: ?limit=1000. validate এখানেই থামাল। next() ডাকা হলো না, তাই handler, database কিছুই ছোঁয়া হলো না। উত্তর 422, বার্তা Too big: expected number to be <=100.

৩ · Params আর query — সবই string, তাই z.coerce

JSON-এ সংখ্যা সংখ্যাই থাকে, কিন্তু URL শুধু লেখা বহন করে। মুদি দোকানের ফর্দের মতো: “চাল ৫” লেখা থাকলেও “৫” একটি লেখা, হিসাব করতে হলে আগে সংখ্যা ভাবতে হয়। z.coerce.number() ঠিক সেই কাজটি করে। একে বলে coercion. ভেতরে সে JavaScript-এর Number(value) ডাকে, ফলে Number-এর সব অদ্ভুত আচরণও সঙ্গে আসে। নিচের বাক্সে আগে সেটি দেখুন, তারপর সীমাসহ একটি ছোট শেখার মডেল।

// z.coerce.number() ভেতরে Number(v) ডাকে, তাই আগে Number-এর আচরণ দেখি
for (const raw of ['2', '', ' 7 ', '1e3', 'abc']) console.log(JSON.stringify(raw), '->', Number(raw));
console.log('Boolean("false") ->', Boolean('false'));

function intParam(raw, { def, min, max }) {
  if (raw === undefined) return { value: def };
  const n = Number(raw);
  if (!Number.isInteger(n)) return { error: 'expected integer' };
  if (n < min) return { error: `min ${min}` };
  if (n > max) return { error: `max ${max}` };
  return { value: n };
}
const pageRule = { def: 1, min: 1, max: 1000 };
const limitRule = { def: 20, min: 1, max: 100 };

function list(path) {
  const q = new URL(path, 'http://x').searchParams;
  const get = (k) => (q.has(k) ? q.get(k) : undefined);   // key নেই মানে undefined
  const page = intParam(get('page'), pageRule);
  const limit = intParam(get('limit'), limitRule);
  if (page.error || limit.error) return `422 page: ${page.error ?? 'ok'}, limit: ${limit.error ?? 'ok'}`;
  return `200 page=${page.value} limit=${limit.value} offset=${(page.value - 1) * limit.value}`;
}
for (const path of ['/p', '/p?page=3&limit=20', '/p?limit=100000', '/p?page=0', '/p?page=abc&limit=2.5']) {
  console.log(path.padEnd(24), list(path));
}

প্রথম ভাগে "" -> 0, " 7 " -> 7, "1e3" -> 1000, "abc" -> NaN, আর Boolean("false") -> true. দ্বিতীয় ভাগে খালি URL পেল ডিফল্ট 200 page=1 limit=20 offset=0. page=3&limit=20-তে offset (৩ − ১) × ২০ = ৪০. limit=100000 হলো 422 page: ok, limit: max 100. শেষ লাইনে দুটি ভুলই একসাথে এলো: 422 page: expected integer, limit: expected integer.

এই আচরণ আসল Zod-এ কেমন? মডেল নয়, Zod 4.6.7 চালিয়ে পাওয়া ফল:

সত্যিকারের run · z.coerce · Zod 4.6.7

ইনপুটz.coerce.number()মানে
"12", " 12 "12ঠিক আছে, ফাঁকাও ক্ষমা পায়
""0ফাঁদ: খালি ঘর শূন্য হয়ে গেল
null0ফাঁদ: JSON body-তে বিপজ্জনক
"1e3", "0x10"1000, 16বিস্ময়কর, কিন্তু সীমা ধরে রাখে
"abc"ভুল, received NaNঠিক আচরণ
"৫" (বাংলা অঙ্ক)ভুল, received NaNবাংলা ইনপুট থাকলে আগে অঙ্ক বদলান
"1e999"ভুল, received Infinityঠিক আচরণ

Express-এর route-এ সত্যিকারের run-এ ?page= ধরা পড়ল min(1)-এ, কারণ "" হলো 0. কিন্তু min না থাকলে, যেমন minPrice ঘরে, ?minPrice= নীরবে “০ টাকার ওপরে” হয়ে যেত।

সবচেয়ে বড় ফাঁদ boolean-এ। z.coerce.boolean() ডাকে Boolean(v), তাই সত্যিকারের run-এ "false" আর "0" দুটিই দিল true. শুধু "" দিল false. অর্থাৎ ?inStock=false উল্টো কাজ করবে। URL-এর boolean-এর জন্য Zod 4-এ আছে z.stringbool(): "true", "1", "yes" হলো true; "false", "0" হলো false; আর "maybe" হলো ভুল।

একই key দুবার, আর array

Express 5-এর ডিফল্ট query parser সরল। সত্যিকারের run-এ ?a[b]=1&c=1&c=2 দিল {"a[b]":"1","c":["1","2"]}. বন্ধনীর nested object আর হয় না (Express 4-এ হতো), কিন্তু একই key দুবার এলে array হয়। তাই ?q=a&q=b-তে handler হঠাৎ array পেতে পারে। Schema থাকলে সমস্যা নেই, সত্যিকারের run-এ উত্তর এলো expected string, received array, আর ?page=1&page=2-এ received NaN.

Pagination-এর সীমা: একটি হিসাব

Pagination-এর ওপরের সীমা কোনো সৌন্দর্য নয়, এটি আপনার সবচেয়ে সস্তা সুরক্ষা। ধরে নিন বাজারঘরে ২,০০,০০০টি পণ্য, আর প্রতিটি JSON সারি গড়ে ৫০০ byte.

সীমা ছাড়া ?limit=1000000: ২,০০,০০০ × ৫০০ = ১০,০০,০০,০০০ byte, প্রায় ১০০ MB
সীমা ১০০ হলে: ১০০ × ৫০০ = ৫০,০০০ byte, প্রায় ৫০ KB
পার্থক্য: ২,০০০ গুণ, একটিমাত্র .max(100) দিয়ে

page-এরও সীমা রাখা হয়েছে ১০০০, কারণ page=1000&limit=100 মানে offset (১০০০ − ১) × ১০০ = ৯৯,৯০০. Database-কে এতগুলো সারি পেরিয়ে যেতে হয়। এর খরচ আর cursor pagination আসবে পর্ব ৩৪-এ (Pagination, filter আর sort).

৪ · অজানা key আর mass assignment

একজন ক্রেতা নিজের প্রোফাইলে নাম বদলাচ্ছেন। Browser পাঠায় { name }. কিন্তু কেউ curl-এ লিখলেন { name, role: 'admin', walletBalance: 999999 }. Handler-এ যদি থাকে Object.assign(user, req.body), তাহলে ভেতরে যা ঘটে তার নাম mass assignment: client-এর সব key মডেলে বসে গেল। ফরমে যে ঘর ছিলই না, সেটিও বদলে গেল।

Zod-এর z.object ডিফল্টে অচেনা key ফলাফলে তোলে না। এই আচরণটিই এই ভুলের ওষুধ, তবে শর্ত আছে: handler-কে req.body নয়, schema-র ফল ব্যবহার করতে হবে। সত্যিকারের run-এ একই payload দুটি route-এ পাঠিয়ে দেখুন।

  1. Payloadrole: adminবাড়তি key
  2. ভুল routereq.bodyসব বসল
  3. Schemastripচেনা key
  4. ঠিক routeinput.bodyনিরাপদ
mass.jsonকেউ curl দিয়ে পাঠালেন
{ "name": "Rahima",
  "role": "admin",
  "walletBalance": 999999 }
ভুল routeশেখার জন্য
app.patch('/bad/me', (req, res) => {
  Object.assign(user, req.body);
  res.json(user);
});
সত্যিকারের উত্তরPATCH /bad/me · 200
{"id":7,"name":"Rahima",
 "role":"admin","walletBalance":999999,
 "city":"dhaka"}
Schemaনতুন object, শুধু চেনা key
profileBody = z.object({
  name: z.string().trim().min(2).max(60),
  city: z.enum([...]).optional(),
})
// ফল: { name: 'Rahima' }
সত্যিকারের উত্তরPATCH /api/me · 200
app.patch('/api/me', validate({ body: profileBody }), …
  Object.assign(user, res.locals.input.body);

{"id":7,"name":"Rahima",
 "role":"customer","walletBalance":50,"city":"dhaka"}
পরের স্তরএকটি পাহারায় ভরসা নয়
await repo.updateProfile(id, {
  name: input.name,      // ঘরগুলো নাম ধরে লেখা
  city: input.city,
});
  1. Payload. JSON বৈধ। name ঠিক আছে, শুধু দুটি বাড়তি key. Status code দিয়ে এই request-কে আলাদা করা যাবে না, তাই এটি ধরা কঠিন।
  2. ভুল route. Object.assign জানে না কোন key নিরাপদ। সে সবই বসায়। ORM-এ update({ data: req.body }) লিখলেও একই ঘটনা।
  3. ক্ষতি। সত্যিকারের উত্তরে role এখন admin, আর wallet ৯,৯৯,৯৯৯. Status 200, কোনো error নেই, log-ও নীরব।
  4. Schema. এবার validate চলল। Zod ফলাফলে নতুন object বানায়, তাই role আর walletBalance সেখানে ওঠেই না। একেই বলে allowlist: কী নেবেন তা বলা, কী নেবেন না তা অনুমান করা নয়।
  5. ঠিক route. নতুন করে চালু করা server-এ একই payload, কিন্তু role রইল customer, wallet রইল ৫০. উত্তরও 200, শুধু ক্ষতি নেই।
  6. দ্বিতীয় স্তর। কেউ পরে schema-য় ভুলে role যোগ করতে পারেন। তাই repository-তেও ঘরগুলো নাম ধরে লিখুন। ভূমিকা বদলানোর জন্য আলাদা admin route আর permission থাকবে, যা আসবে পর্ব ৪৮-এ (Permission).

অচেনা key নিয়ে কী করবেন, Zod তিনটি পথ দেয়। একই ডেটা { name: 'Rahima', role: 'admin' } তিন রকম schema-য়:

সত্যিকারের run · Zod 4.6.7

z.object       {"name":"Rahima"}                    // strip: নীরবে বাদ
z.strictObject ERR Unrecognized key: "role"          // strict: বাতিল
z.looseObject  {"name":"Rahima","role":"admin"}     // loose: সব রাখে

Strict বাড়তি key পেলে ভুল ধরিয়ে দেয়, তাই নিজের client-এর বানান ভুলও ধরা পড়ে। Strip নতুন-পুরোনো client-কে সহজে চলতে দেয়। দুটিই নিরাপদ, পছন্দটি নিয়ে আলোচনা আছে ভাবনার প্রশ্ন ০৩-এ। looseObject কোনো database-লেখার পথে ব্যবহার করবেন না। নিচের মডেলে পার্থক্যটি, আর একটি বাড়তি ফাঁদ prototype pollution দেখুন।

const newUser = () => ({ id: 7, name: 'Rahima', role: 'customer', isAdmin: false });
const body = { name: 'Rahima Begum', role: 'admin', isAdmin: true };

// ভুল পথ: client যা পাঠাল, সব বসিয়ে দেওয়া
const hurt = Object.assign(newUser(), body);
console.log('blind   ', JSON.stringify(hurt));

// strip: চেনা key নতুন object-এ তোলে, বাকি নীরবে বাদ
const shape = { name: 'string', city: 'string?' };
function strip(input) {
  const out = {};
  for (const key in shape) if (key in input) out[key] = input[key];
  return out;
}
const safe = Object.assign(newUser(), strip(body));
console.log('strip   ', JSON.stringify(safe));

// strict: অচেনা key পেলেই পুরো request বাতিল
function strict(input) {
  const unknown = Object.keys(input).filter((key) => !(key in shape));
  return unknown.length ? { status: 422, unknown } : { status: 200, data: strip(input) };
}
console.log('strict  ', JSON.stringify(strict(body)));
console.log('strict  ', JSON.stringify(strict({ name: 'Rahima Begum' })));

// Object.assign আর __proto__: JSON থেকে আসা key নিজের key হয়
const evil = JSON.parse('{"name":"R","__proto__":{"isAdmin":true}}');
const target = Object.assign({}, evil);
console.log('proto   ', Object.keys(evil), 'target.isAdmin =', target.isAdmin);
console.log('stripped', Object.keys(strip(evil)), 'isAdmin =', strip(evil).isAdmin);

blind লাইনে "role":"admin","isAdmin":true. strip লাইনে "role":"customer","isAdmin":false, শুধু নাম বদলেছে। strict প্রথমবার দিল {"status":422,"unknown":["role","isAdmin"]}, দ্বিতীয়বার 200. শেষ দুই লাইন গুরুত্বপূর্ণ। JSON.parse __proto__-কে সাধারণ key বানায় ([ 'name', '__proto__' ]), আর Object.assign সেটি বসাতে গিয়ে target-এর prototype বদলে দেয়, তাই target.isAdmin = true. Allowlist-এ __proto__ নেই, তাই ফল isAdmin = undefined. আসল Zod-এর সত্যিকারের run-ও একই: keys [ 'name' ] isAdmin undefined.

৫ · সীমা — size, array আর string

আকৃতি ঠিক হলেই ডেটা নিরাপদ নয়। একটি নিখুঁত JSON-এ ২ লাখ সারি থাকতে পারে। গুদামের দরজায় “মাল আসবে” বলা যথেষ্ট নয়, ট্রাকের মাপও বলতে হয়। Server-এর CPU আর memory সীমিত, তাই প্রতিটি ইনপুটের ওপরে একটি ছাদ লাগে। ক্রম এখানে জরুরি: সবচেয়ে সস্তা পরীক্ষা আগে।

  1. Byte-এর সীমা, parse-এর আগে: express.json({ limit }), যাকে বলে body limit. ডিফল্ট 100kb, সত্যিকারের run-এ যা ১,০২,৪০০ byte.
  2. Array-র দৈর্ঘ্য: z.array(…).max(20).
  3. String-এর দৈর্ঘ্য: .max(200), প্রতিটি লেখার ঘরে।
  4. সংখ্যার সীমা: .min(), .max().

নিচের বাক্সে দেখুন, কোন request কতদূর পৌঁছায়। parsed গুনছে JSON.parse কতবার চলল।

const http = require('node:http');
const LIMIT = 100 * 1024;      // express.json-এর ডিফল্ট 100kb
const MAX_ITEMS = 20;
let parsed = 0;

const server = http.createServer(async (req, res) => {
  const reply = (code, msg) => res.writeHead(code, { Connection: 'close' }).end(msg);
  if (Number(req.headers['content-length']) > LIMIT) {
    req.resume();              // শরীর ফেলে দেওয়া হচ্ছে, জমছে না
    return reply(413, `too large, limit ${LIMIT} bytes`);
  }
  const chunks = [];
  for await (const c of req) chunks.push(c);
  const body = JSON.parse(Buffer.concat(chunks));
  parsed++;
  if (!Array.isArray(body.items)) return reply(422, 'items: expected array');
  if (body.items.length > MAX_ITEMS) return reply(422, `items: max ${MAX_ITEMS}`);
  reply(201, `ok, ${body.items.length} lines`);
});

server.listen(0, '127.0.0.1', async () => {
  const url = `http://127.0.0.1:${server.address().port}/api/orders`;
  for (const n of [3, 21, 200000]) {
    const items = Array.from({ length: n }, () => ({ sku: 'dal-1kg', qty: 1 }));
    const text = JSON.stringify({ items });
    const r = await fetch(url, { method: 'POST', body: text });
    console.log(`${n} lines, ${text.length} bytes ->`, r.status, await r.text(), `| parsed so far: ${parsed}`);
  }
  server.close();
});

তিনটি লাইন: 3 lines, 89 bytes -> 201 ok, 3 lines | parsed so far: 1, 21 lines, 557 bytes -> 422 items: max 20 | parsed so far: 2, আর 200000 lines, 5200011 bytes -> 413 too large, limit 102400 bytes | parsed so far: 2. ২১ সারির request parse হয়ে তারপর আটকাল। প্রায় ৫২ লাখ byte-এর request-এ গণনা বাড়েনি: JSON.parse একবারও চলেনি, শুধু header দেখেই ফেরত।

আসল Express আর Zod-এ সত্যিকারের ফল একই। ৫২ লাখ byte-এ {"code":"TOO_LARGE","message":"body limit is 102400 bytes"}, status 413. ২১ সারিতে 422, সর্বোচ্চ ২০টি লাইন. একটি স্থানীয় run-এ এই ৫২ লাখ byte-এর JSON.parse নিল প্রায় ২৬ ms, যা body limit পুরো বাঁচিয়ে দিল। সংখ্যাটি একটি মেশিনের, সাধারণ ফল নয়।

.max() সরিয়ে দিলে কী হয়, মেপে দেখা হলো। ২ লাখটি ভুল সারি, Zod 4.6.7, একটি স্থানীয় run. .max(20) থাকলে ভুল ১টি, প্রায় ০ ms, কারণ দৈর্ঘ্য ছাড়ালে Zod সারিগুলো দেখেনি। .max না থাকলে ভুল ৪,০০,০০০টি, প্রায় ২০০ ms, আর সেই ভুলের তালিকার JSON প্রায় ৩৮ MB. অর্থাৎ ৫ MB পাঠিয়ে ৩৮ MB উত্তর আনা যেত। Error-এর তালিকাও একটি উত্তর, আর তারও ছাদ লাগে।

কতটুকু সীমা যথেষ্ট? হিসাব করে নিন

অনুমান নয়, আপনার সবচেয়ে বড় বৈধ request মাপুন। UTF-8-এ বাংলা অক্ষর সাধারণত ৩ byte, কিন্তু JavaScript-এর .length, তাই Zod-এর .max()-ও, গোনে UTF-16 একক। সত্যিকারের run-এ “রহিমা”-র length ৫, byte ১৫. 😀-এর length ২, byte ৪.

Review: ২,০০০ বাংলা অক্ষর × ৩ = ৬,০০০ byte
সর্বোচ্চ বৈধ order: ২০ সারি, ৪০ অক্ষরের sku, ২০০ অক্ষরের বাংলা note = ১,৮৪৩ byte (সত্যিকারের run)
১,০২,৪০০ ÷ ১,৮৪৩ ≈ ৫৫ গুণ বেশি জায়গা

তাই ডিফল্টের ওপর ভরসা না করে route অনুযায়ী ছোট সীমা দেওয়া যায়, যেমন order-এর route-এ express.json({ limit: '8kb' }). তখন বিশ্বব্যাপী app.use-এ না রেখে route-এর আগে বসাতে হবে। File upload আলাদা পথ, যা আসবে পর্ব ৩০-এ (File upload).

Regex-ও একটি খরচ। (a+)+$-এর মতো nested quantifier কিছু লেখায় সময় বিস্ফোরিত করে। একে বলে ReDoS. ফোনের regex-এর মতো সরল, ^…$ দিয়ে বাঁধা নকশা রাখুন, আর AI-এর দেওয়া জটিল regex যাচাই ছাড়া নেবেন না। এর পাশাপাশি rate limit একটি আলাদা স্তর, যা আছে system design-এর rate limiting পর্বে.

৬ · ভুলের একই ছাঁচ — 400, 413, 415, 422

Frontend-এর কোড একটিই। Server যদি কখনো { error }, কখনো { message }, কখনো HTML পাঠায়, তাহলে client-এ প্রতিটির আলাদা শর্ত লাগে। ব্যাংকের ফরম ফেরত দেওয়ার মতো: কর্মকর্তা প্রতিবার একই ছকে লেখেন কোন ঘরে কী ভুল। এই পর্বের প্রতিটি ফিরতি উত্তরে তাই code আছে, আর যাচাইয়ের ভুলে errors তালিকা। পর্ব ২১-এর form ঠিক এই path ধরেই setError ডাকে, তাই ভুলটি সঠিক ঘরে বসে।

Statusকখনকে ধরেসত্যিকারের code
400 Bad RequestJSON-ই পড়া যায় না ({bad)express.jsonBAD_JSON
413 Payload Too LargeBody সীমার বড়express.jsonTOO_LARGE
415 Unsupported Media Typetext/plain বা body নেইvalidateJSON_REQUIRED
422 Unprocessable EntityJSON ঠিক, ডেটা নিয়ম ভেঙেছেvalidateVALIDATION_FAILED
404 Not FoundID ঠিক, জিনিসটি নেইHandlerNOT_FOUND
409 Conflictডেটা ঠিক, এখনকার অবস্থায় হবে নাServiceOUT_OF_STOCK

একটি ছোট কথা: RFC 9110 (২০২২) 413 আর 422-এর নাম দিয়েছে Content Too Large আর Unprocessable Content, কিন্তু Node এখনো পুরোনো নাম ছাপে, তাই আউটপুটে Payload Too Large আর Unprocessable Entity দেখবেন। 422 এসেছিল WebDAV-এর RFC 4918 (২০০৭) থেকে।

400 না 422? এই নিয়ে দুই মতই চালু। অনেক API সব ইনপুটের ভুলে 400 দেয়, যা সহজ। এই কোর্সে 400 মানে পড়াই যায়নি, 422 মানে পড়া গেছে কিন্তু নিয়ম ভেঙেছে। দলে যেটি ঠিক করবেন, সব route-এ সেটিই রাখুন। ধারাবাহিকতাই আসল। নকশার যুক্তি আছে পর্ব ৩৩-এ (Status code আর error format), আর system design-এর REST API পর্বে.

400 আর 413-র ভুল validate-এ পৌঁছায় না, কারণ সেগুলো express.json-এর ভেতরেই ঘটে আর error middleware-এ যায়। Handler না থাকলে Express নিজের HTML পাতা পাঠাত, যা এই ছাঁচ ভাঙে। তাই একটি ছোট error middleware লাগে:

const onError: ErrorRequestHandler = (err, _req, res, next) => {
  if (res.headersSent) return next(err);
  if (err.type === 'entity.parse.failed') {
    return res.status(400).json({ code: 'BAD_JSON', message: 'body is not valid JSON' });
  }
  if (err.type === 'entity.too.large') {
    return res.status(413).json({ code: 'TOO_LARGE', message: `body limit is ${err.limit} bytes` });
  }
  res.status(500).json({ code: 'INTERNAL', message: 'something went wrong' });
};
app.use(onError);

সত্যিকারের run · একই server · header ছাঁটা

$ curl -si -X POST -H 'content-type: application/json' -d '{bad' …/api/orders
HTTP/1.1 400 Bad Request
{"code":"BAD_JSON","message":"body is not valid JSON"}

$ curl -si -X POST -H 'content-type: text/plain' --data-binary @good.json …/api/orders
HTTP/1.1 415 Unsupported Media Type
{"code":"JSON_REQUIRED","message":"send application/json"}

$ curl -si -X POST … -H 'Idempotency-Key: 123' --data-binary @good.json …/api/orders
HTTP/1.1 422 Unprocessable Entity
{"code":"VALIDATION_FAILED","errors":[{"in":"headers","path":"idempotency-key",
"message":"Idempotency-Key একটি UUID হতে হবে"}]}

$ curl -si …/api/products/abc
HTTP/1.1 422 Unprocessable Entity
{"code":"VALIDATION_FAILED","errors":[{"in":"params","path":"id",
"message":"Invalid input: expected number, received NaN"}]}

$ curl -si …/api/products/99
HTTP/1.1 404 Not Found
{"code":"NOT_FOUND","message":"product not found"}

415 চেকটি প্রথমে কেন? প্রথম সংস্করণে সেটি ছিল না। তখন text/plain-এ পাঠালে সত্যিকারের উত্তর ছিল {"in":"body","path":"","message":"Invalid input: expected object, received undefined"}. Content-Type না মিললে express.json body ছোঁয় না, আর Express 5-এ req.body তখন undefined, পুরোনো Express-এর মতো {} নয়। validate না থাকলে req.body.items সরাসরি crash করত। এই ছাঁচকে একটি মানে সাজায় Problem Details (RFC 9457), যা আসবে পর্ব ৩৩-এ। Central error handler-এর পূর্ণ রূপ পরের পর্বে।

৭ · Type schema থেকে, আর আকৃতি বনাম ব্যবসার নিয়ম

validate-এর জেনেরিক কোডটি এতক্ষণ চুপচাপ একটি বড় উপহার দিচ্ছিল। Type inference-এর ফলে handler-এর res.locals.input.body-এর type আপনাকে লিখতে হয় না, schema থেকেই আসে। হাতে কোনো interface Order নেই, অথচ ভুল করলে সঙ্গে সঙ্গে ধরা পড়ে:

app.post('/api/orders', validate({ body: orderBody }), (req, res) => {
  const { items, discount } = res.locals.input.body;
  const first: string = items[0].qty;
  res.json({ first, discount });
});

সত্যিকারের run · TypeScript 7.0.2 · npx tsc --noEmit

src/typedemo.ts(9,18): error TS2339: Property 'discount' does not exist on type
  '{ phone: string; items: { sku: string; qty: number; }[]; note?: string | undefined; }'.
src/typedemo.ts(10,9): error TS2322: Type 'number' is not assignable to type 'string'.

নিয়ম আর type এখন একটিই জিনিস, তাই আলাদা হতে পারে না। একটি সূক্ষ্মতা: coerce করা schema-র দুই মুখ। z.output<typeof listQuery>-এ page হলো number, কিন্তু z.input-এ সেটি unknown, কারণ coerce যেকোনো কিছু নিতে রাজি। সত্যিকারের run-এ { page: 'abc' } z.input-এ পাস করল, z.output-এ করল না। z.infer মানে z.output. Handler-এ তাই output-ই চাই।

কোন নিয়ম কোথায়

ব্যাংকের চেক কাউন্টার ভাবুন। কেরানি প্রথমে দেখেন তারিখ আছে, অঙ্ক আর কথায় টাকা মিলছে, স্বাক্ষরের ঘর ভরা। এটি আকৃতি: চেকটি দেখেই বলা যায়, হিসাব-খাতা লাগে না। তারপর তিনি খাতা খুলে দেখেন হিসাবে টাকা আছে কি না। এটি ব্যবসার নিয়ম: ডেটা বৈধ, কিন্তু এখনকার অবস্থার ওপর উত্তর নির্ভর করে।

সরল পরীক্ষা

নিয়মটি যাচাই করতে কি database বা অন্য কোনো বর্তমান অবস্থা লাগে? না হলে schema-তে। হ্যাঁ হলে service-এ। Schema-র ভেতরে database কল করলে প্রতিটি যাচাই ধীর হয়, schema browser-এ আর ভাগ করা যায় না, আর business logic দুই জায়গায় ছড়িয়ে যায়।

তিনটি request একই route-এ পাঠিয়ে দেখুন। ডালের মজুত ২টি। db গুনছে database-এর কাজ।

1// route: আকৃতি2validate({ body: orderBody })3// service: ব্যবসার নিয়ম4const stock = await repo.stock(sku);5if (qty > stock) throw new OutOfStock(sku, stock);6await repo.saveOrder(order);
এই মুহূর্তের মান
qty
schema
stock
db
  1. Request ১: qty: 99. Schema-র .max(10) ভাঙল। Database-এর দরকারই নেই, কারণ ১০-এর বেশি কখনোই বৈধ নয়।
  2. 422, সস্তায়। Service চলেনি, db ০. আকৃতির ভুল এভাবেই সবচেয়ে কম খরচে ফেরে।
  3. Request ২: qty: 3. ১ থেকে ১০-এর মধ্যে, তাই schema পাস। Schema জানে না গুদামে কত আছে, আর জানার কথাও নয়।
  4. Service খাতা খুলল। মজুত পড়া হলো, stock ২. db ১.
  5. 409. ৩ > ২, তাই service আটকাল। ডেটা ঠিক, অবস্থা মিলছে না। সত্যিকারের উত্তর {"code":"OUT_OF_STOCK","errors":[{"path":"items.0.qty","message":"মজুত আছে 2টি"}]}. path একই ছাঁচে, তাই form ঘরটি খুঁজে পায়।
  6. Request ৩: qty: 2. Schema পাস। নতুন request, তাই গণনা নতুন করে শুরু।
  7. Service পরীক্ষা। ২ ≤ ২, নিয়ম পাস, db ১.
  8. সংরক্ষণ। Order জমা হলো, মজুত ০, db ২. মোট ১৪০ × ২ = ২৮০, যা server নিজে হিসাব করেছে। দুজন একসাথে শেষ ২টি নিতে চাইলে এই “পড়ে, তারপর লেখা” ফাঁক রাখে। সমাধান transaction, যা আসবে পর্ব ৪৩-এ।
নিয়মকোথায়কেন
ফোনের ধরন, qty ১–১০, sort-এর তালিকাSchemaডেটা দেখেই বলা যায়
বিকাশ হলে trxId লাগবেSchema (refine)একই request-এর ঘরের সম্পর্ক
পণ্যটি আছে কি না, মজুত, দামServiceDatabase লাগে, অবস্থা বদলায়
Coupon-এর মেয়াদ, এই ক্রেতার প্রাপ্য কি নাServiceসময় আর ব্যবহারকারী লাগে
এই order কি এই ক্রেতার?Service বা authorization স্তরপর্ব ৪৮-এর বিষয়, schema পারে না
ফোন নম্বর unique কি নাDatabase constraintRace-এ শেষ রায়

Service-কে কীভাবে route থেকে আলাদা করবেন, তা আসবে পর্ব ২৯-এ (Backend-এর গঠন). শেষ নিয়মটি মনে রাখবেন: database-এর constraint-ও একটি যাচাই, সবচেয়ে শেষের, আর সবচেয়ে নির্ভরযোগ্য।

৮ · Sanitize না validate — একটি জমা review-এর HTML

দুটি শব্দ প্রায়ই গুলিয়ে যায়। Validate মানে ডেটা দেখে হ্যাঁ বা না বলা, ডেটা বদলানো নয়। Sanitize মানে ডেটা বদলে নিরাপদ করা। আর তৃতীয়টি, escape, মানে ডেটা না বদলে যেখানে বসছে সেখানকার ভাষায় নিরীহভাবে লেখা। .trim() ছোট একটি sanitize, যা নির্দোষ। কিন্তু ইনপুট থেকে “বিপজ্জনক” অক্ষর কেটে ফেলা ভুল পথ।

কেন? বাজারঘরের একটি review পড়ুন, যেখানে শেষের অংশটি পরীক্ষার জন্য বসানো নিরীহ লেখা:

দাম <১০০ টাকা, ভালো ডাল <script>alert(1)</script>

< অক্ষরটি নিষিদ্ধ করলে “দাম <১০০ টাকা” লেখাটি নষ্ট হয়। Schema-র কাছে এটি একটি বৈধ string, ৩ থেকে ২,০০০ অক্ষরের মধ্যে, তাই যাচাই ঠিকভাবেই পাস করল। বিপদ যাচাইয়ে নয়, ব্যবহারে। Database-এ HTML কোনো কোড নয়, শুধু লেখা। এই লেখা কোনো পাতায় escape ছাড়া বসলে তবেই browser সেটিকে script ভেবে চালায়। একে বলে stored XSS. একই জমা review, দুটি পাতায়:

const escapeHtml = (s: string) => s.replace(/[&<>"']/g, (c) => ({
  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
}[c]!));

// ভুল পাতা: শেখার জন্য
app.get('/bad/reviews', (_req, res) => {
  res.type('html').send(`<ul>${reviews.map((r) => `<li>${r.text}</li>`).join('')}</ul>`);
});

app.get('/reviews', (_req, res) => {
  res.type('html').send(`<ul>${reviews.map((r) => `<li>${escapeHtml(r.text)}</li>`).join('')}</ul>`);
});

সত্যিকারের run · একই জমা review · header ছাঁটা

$ curl -si …/api/products/2/reviews -X POST -H 'content-type: application/json' --data-binary @review.json
HTTP/1.1 201 Created
{"saved":{"rating":5,"text":"দাম <১০০ টাকা, ভালো ডাল <script>alert(1)</script>"}}

$ curl -si …/bad/reviews
HTTP/1.1 200 OK
<ul><li>দাম <১০০ টাকা, ভালো ডাল <script>alert(1)</script></li></ul>

$ curl -si …/reviews
HTTP/1.1 200 OK
<ul><li>দাম &lt;১০০ টাকা, ভালো ডাল &lt;script&gt;alert(1)&lt;/script&gt;</li></ul>

লেখাটি অক্ষত রইল, “<১০০” পাঠকের চোখে ঠিকই দেখা যাবে, অথচ script আর script রইল না। মনে রাখুন, পর্ব ০৮-এ দেখেছেন React-এর {text} নিজে থেকেই এই কাজ করে। তাই dangerouslySetInnerHTML ছাড়া React পাতা এই ভুল থেকে সুরক্ষিত। Express-এ template engine ব্যবহার করলে তার ডিফল্ট escape চালু আছে কি না দেখে নিন।

বিপদপ্রধান প্রতিকারযাচাইয়ের ভূমিকাগভীরে
HTML, script (XSS)Output-এ context অনুযায়ী escape, CSPদৈর্ঘ্যের সীমা, কম ক্ষতিXSS ও CSP
SQL injectionParameterized queryID সংখ্যা, sort তালিকাInjection প্রতিরক্ষা
Client-এর পাঠানো object দিয়ে NoSQL query বদলানোDriver-কে শুধু নির্দিষ্ট মান দেওয়াz.string() object ফেরায়API security
Rich text, যেখানে HTML সত্যিই লাগবেপরীক্ষিত HTML sanitizer libraryনিজে regex লিখবেন নাXSS ও CSP
// মান সব সময় আলাদা, SQL লেখায় নয়
const rows = await db.query(
  `SELECT * FROM products WHERE price < $1 ORDER BY ${SORT_COLUMNS[sort]} LIMIT $2 OFFSET $3`,
  [maxPrice, limit, offset],
);

// ORDER BY-তে placeholder চলে না, তাই schema-র enum + এই তালিকা
const SORT_COLUMNS = { price: 'price', name: 'name', new: 'created_at' } as const;

এখানে যাচাই আর parameterized query একসাথে কাজ করছে। Column-এর নাম placeholder-এ বসে না, তাই sort-এর z.enum আর SORT_COLUMNS মিলে allowlist হলো। এটি বিস্তারিত আছে injection প্রতিরক্ষা পর্বের dynamic ORDER BY অংশে। যাচাই একটি বাড়তি স্তর, প্রধান প্রতিকার নয়: text-এর মতো মুক্ত লেখায় ' বৈধ অক্ষর, তাকে আটকানো যায় না, parameterized query-ই সেখানে একমাত্র ঠিক পথ।

নিয়ম এক লাইনে: ঢোকার সময় যাচাই, বের হওয়ার সময় escape. ডেটা জমার সময় একবার escape করে রাখলে JSON API, email আর mobile app-এ &lt; দেখা যাবে, আর একই লেখা কোন জায়গায় বসবে তা তখনো জানা নেই। প্রতিটি context-এর নিয়ম আলাদা।

৯ · Schema থেকে JSON Schema আর OpenAPI

আপনার Zod schema এখন যাচাই আর type দুটোই দিচ্ছে। Python-এর কোনো সেবা, মোবাইল দল, বা API-র docs কিন্তু TypeScript পড়ে না। Zod 4-এ schema থেকে ভাষানিরপেক্ষ JSON Schema বানানো যায় z.toJSONSchema() দিয়ে, কোনো বাড়তি package ছাড়াই।

সত্যিকারের run · Zod 4.6.7 · ছোট করে সাজানো · z.toJSONSchema(item)

{
 "$schema": "https://json-schema.org/draft/2020-12/schema",
 "type": "object",
 "properties": {
  "qty": { "type": "integer", "minimum": 1, "maximum": 10 },
  "sku": { "type": "string", "maxLength": 40 }
 },
 "required": [ "qty", "sku" ],
 "additionalProperties": false
}

additionalProperties: false লক্ষ করুন। Runtime-এ z.object অচেনা key নীরবে বাদ দেয়, কিন্তু এই ডিফল্ট রূপটি ডেটার output-এর বর্ণনা। { io: 'input' } দিলে ইনপুটের রূপ আসে, যেখানে সত্যিকারের run-এ default দেওয়া ঘরগুলো required-এ থাকল না। Docs বানানোর সময় তাই ঠিক দিকটি বেছে নিন।

পরের ধাপ OpenAPI, যা পুরো API-র চুক্তি। লেখার সময়ে zod-openapi (6.0.2) আর @asteasolutions/zod-to-openapi (9.1.0) ধরনের package-গুলো Zod 4-কে peer হিসেবে দেখায়, যা npm view দিয়ে দেখা হয়েছে। এগুলো এখানে চালানো হয়নি, উদাহরণ হিসেবে নাম দেওয়া হলো। OpenAPI-র পুরো আলোচনা পর্ব ৩৫-এ (Versioning আর OpenAPI), আর typed client পর্ব ৩৭-এ।

আরও গভীরে: “Parse, don't validate” ভাবনার আগে থেকেই LangSec নামের গবেষণাধারা, যা Meredith Patterson ও Len Sassaman-এর কাজের সঙ্গে যুক্ত বলে পরিচিত, বলে আসছে: ইনপুট সীমানায় পুরোপুরি চিনে নিন, কোডের মাঝখানে ছড়ানো ছোট ছোট চেক (তাঁদের ভাষায় “shotgun parser”) ভুলের উৎস। আরেকটি বিষয় JSON-এর নিজের: RFC 8259 একই key দুবার এলে আচরণ নির্দিষ্ট করে না, আর Node-এর JSON.parse-এ শেষেরটি জেতে। তাই দুই ভিন্ন parser একই body ভিন্নভাবে পড়তে পারে। Performance দরকার হলে Ajv-এর মতো JSON Schema validator আগে থেকে কোড বানিয়ে নেয়, তবে সেই লাভ সত্যিই লাগছে কি না আগে মাপুন।

পরীক্ষা ও ইন্টারভিউতে:

  • TypeScript থাকলে runtime যাচাই কেন? Type compile-এর পরে মুছে যায়, বাইরের ডেটা চলার সময়ে আসে।
  • 400 আর 422-র পার্থক্য? 400 মানে request পড়াই গেল না; 422 মানে পড়া গেছে, কিন্তু নিয়ম ভেঙেছে। অনেক API দুটির বদলে 400 রাখে, ধারাবাহিকতা জরুরি।
  • Mass assignment ঠেকাবেন কীভাবে? Allowlist schema, schema-র ফল ব্যবহার, repository-তে ঘর নাম ধরে লেখা।
  • Validation, sanitization, escaping-এর পার্থক্য? হ্যাঁ/না বলা, ডেটা বদলানো, আর ব্যবহারের জায়গায় নিরীহভাবে লেখা।
  • মজুত কি schema-য় যাচাই হবে? না, এটি অবস্থার ওপর নির্ভর করে, তাই service-এ, 409 সহ।

১০ · AI-এর যুগে request যাচাই

AI-এর লেখা Express কোডে route চলে, ভালো request-ও পাস করে। সমস্যা ধরা পড়ে ভুল request-এ, যা সে কখনো চালায়নি। পুরোনো টিউটোরিয়াল থেকে শেখা কোড নতুন version-এ crash করে, এই পর্বের দুটি উদাহরণ তারই সত্যিকারের প্রমাণ।

AI-এর ভুলফলকীভাবে ধরবেন
req.query = parsedExpress 5-এ TypeError, 500Query সহ route চালান
Zod 3-এর error.errorsZod 4-এ undefined (সত্যিকারের run), .map-এ crashভুল request পাঠান; error.issues ব্যবহার করুন
Schema চলে, কিন্তু handler পড়ে req.bodyStrip কাজে লাগে না, mass assignmentisAdmin যোগ করে পাঠান
z.coerce.boolean() URL-এ?inStock=false উল্টোfalse পাঠিয়ে দেখুন; z.stringbool()
limit, .max() বাদএক request-এ পুরো টেবিল?limit=1000000 পাঠান
নেই এমন validator package বা optionInstall ব্যর্থ, অথবা অচেনা package ঢোকেnpm view, official docs
Schema-র ভেতরে database কলধীর, schema ভাগ করা যায় নানিয়মটি service-এ সরান
“Sanitize” বলে < মুছে ফেলালেখা নষ্ট, XSS থেকেই যায়Output-এ escape আছে কি না দেখুন

AI-কে version, উত্তরের ছাঁচ আর সীমাগুলো স্পষ্ট করে বলুন। নইলে সে ধরে নেবে ভালো request-ই আসবে।

আমি Express 5 আর Zod 4 ব্যবহার করছি, TypeScript strict (package.json নিচে দিলাম)। [এখানে package.json] বাজারঘরের POST /api/orders আর GET /api/products-এর জন্য একটি validate(schemas) middleware আর schema লিখে দাও। শর্ত: ১. params, query, headers, body আলাদা schema; ফল res.locals.input-এ, req.query-তে লিখবে না। ২. ভুলে 422 আর { code, errors: [{ in, path, message }] }। ৩. প্রতিটি string, array আর সংখ্যায় ওপরের সীমা; limit সর্বোচ্চ 100. ৪. URL-এর সংখ্যায় z.coerce, boolean-এ z.stringbool; body-তে coerce নয়। ৫. Handler শুধু schema-র ফল ব্যবহার করবে, req.body নয়। ৬. মজুত, দাম আর database কল schema-তে রাখবে না। ৭. কোনো নতুন package যোগ করবে না, আর যে API নিয়ে নিশ্চিত নও সেটি বলে দেবে। শেষে ভালো ও ভুল request-এর curl কমান্ড দাও, আর বলো প্রতিটিতে কোন status আশা করব।

AI-এর লেখা route হাতে পেলে

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

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

প্র ০১TypeScript তো আছেই। তাহলে Zod-এর মতো আলাদা runtime validator-এর দরকার পড়ল কেন, আর এই ধারা এলো কীভাবে?

TypeScript একটি compile-সময়ের পরীক্ষক। সে দেখে আপনার কোড নিজের সঙ্গে সঙ্গতিপূর্ণ কি না। ২০১২ সালে Microsoft যখন TypeScript আনে, তার একটি নকশার সিদ্ধান্ত ছিল, চলার সময়ের JavaScript-এ বাড়তি কিছু যোগ না করা। তাই type সব মুছে যায়। আপনি req.body as Order লিখলে compiler শুধু আপনার কথা মেনে নেয়, ডেটা দেখে না, কারণ ডেটা তখনো আসেইনি।

Server-এর ডেটা আসে চলার সময়ে, বাইরে থেকে। এখানে compiler-এর কিছু করার নেই। তাই JavaScript জগতে আগে থেকেই runtime validator ছিল। hapi framework-এর চারপাশে গড়ে ওঠা Joi পরিচিত ছিল, JSON Schema-র জন্য ছিল Ajv, আর Express-এ অনেকে express-validator ব্যবহার করতেন। সমস্যা ছিল দ্বৈততা: নিয়ম এক জায়গায়, TypeScript-এর interface আরেক জায়গায়। একটি বদলালে অন্যটি পুরোনো থেকে যেত।

io-ts, পরে Yup-এর type সমর্থন, আর তারপর Colin McDonnell-এর Zod এই ফাঁকটি বুজিয়ে দেয়: schema হলো সাধারণ JavaScript মান, আর TypeScript-এর inference তার থেকে type বের করে। ফলে একটিই উৎস। ২০২৫ সালে এসেছে Zod 4. অন্য বিকল্পও আছে, যেমন ছোট আকারের জন্য Valibot, আর একাধিক library-কে এক interface-এ আনতে Standard Schema.

বিপরীত দিকও ভাবুন। Runtime validator-এর খরচ আছে: প্রতিটি request-এ কিছু CPU, আর browser-এ ভাগ করলে bundle. তবে ওয়েব API-তে এই খরচ সাধারণত database আর নেটওয়ার্কের তুলনায় ছোট। ভেতরের ফাংশন থেকে ফাংশনে যাওয়ার সময় আবার যাচাই করার দরকার নেই, TypeScript সেখানে যথেষ্ট। যাচাই লাগে শুধু সীমানায়: request, webhook, বাইরের API-র উত্তর, এমনকি নিজের ফাইল বা environment variable.

মূল উপলব্ধি: TypeScript ভেতরের সঙ্গতি পাহারা দেয়, schema সীমানা পাহারা দেয়। দুটি এক কাজ করে না, আর schema থেকে type বের করলে দুজন এক সূত্রে বাঁধা পড়ে।

প্র ০২Mass assignment এত সরল ভুল। তবু এটি বড় framework-গুলোতেও কেন ঘটেছে, আর framework-গুলো কী শিখল?

কারণ এটি সুবিধা থেকেই জন্মায়। Rails-এর আদি যুগে User.new(params[:user]) এক লাইনে form থেকে model বানিয়ে দিত। অসাধারণ উৎপাদনশীলতা। কোড সুন্দর, আর সুরক্ষার দায়িত্ব ছিল ডেভেলপারের মনে রাখার ওপর, attr_accessible দিয়ে অনুমোদিত ঘর বলে দেওয়া।

২০১২ সালের মার্চে Egor Homakov এই ঝুঁকি নিয়ে Rails-এর দলের কাছে আলোচনা তোলেন। সাড়া আশানুরূপ না পেয়ে তিনি GitHub-এর একটি mass assignment দুর্বলতা ব্যবহার করে Rails project-এই নিজের নামে একটি commit দেখিয়েছিলেন বলে জানা যায়। GitHub দ্রুত ঠিক করে, আর ঘটনাটি অনেক আলোচনা তৈরি করে।

আসল শিক্ষাটি নকশার। “ডেভেলপার মনে রাখবেন” ধরনের নিরাপত্তা বড় code base-এ টেকে না। Rails 4 (২০১৩) তাই strong parameters আনে: params.require(:user).permit(:name), অর্থাৎ controller-এ স্পষ্ট allowlist, ডিফল্টে কিছুই নয়। ডিফল্টটি উল্টে দেওয়াই আসল সমাধান। Laravel-এর $fillable, Django form-এর fields, এগুলোও একই ভাবনার রূপ, যা আপনি পর্ব ৬৮ আর ৭০-এ দেখবেন।

OWASP API Security Top 10-এর ২০১৯ সালের তালিকায় Mass Assignment আলাদা ঝুঁকি ছিল। ২০২৩ সালের তালিকায় সেটি অতিরিক্ত ডেটা ফাঁসের সঙ্গে মিলে API3, Broken Object Property Level Authorization হয়েছে, কারণ দুটিই “object-এর কোন ঘর কে ছুঁতে পারে” প্রশ্ন। ঠিক এই কারণে looseObject আর update({ data: req.body }) বিপজ্জনক। Response-এও একই সতর্কতা লাগে: res.json(user) করলে passwordHash বেরিয়ে যেতে পারে, তাই response-এর জন্যও আলাদা গড়ন বানান। বিস্তারিত আছে API security পর্বে।

মূল উপলব্ধি: নিরাপত্তার ডিফল্ট এমন হওয়া উচিত যে কিছু না করলে কিছু খোলে না। Client-কে কোন ঘর ছুঁতে দেবেন, সেটি একটি স্পষ্ট সিদ্ধান্ত, দুর্ঘটনা নয়।

প্র ০৩অজানা key নীরবে ফেলে দেবেন, নাকি 4xx দিয়ে ফেরত পাঠাবেন? দুই পক্ষের যুক্তি কী?

দুটিই mass assignment ঠেকায়, তাই এটি নিরাপত্তার নয়, নকশার প্রশ্ন। Strip-এর পক্ষে আছে পুরোনো একটি নীতি। Jon Postel-এর TCP ও IP-র নথি থেকে আসা Robustness Principle বলে: যা পাঠান তাতে কড়া হোন, যা নেন তাতে উদার হোন। ধরুন মোবাইল app-এর নতুন version একটি নতুন ঘর পাঠাচ্ছে, কিন্তু server এখনো পুরোনো। Strip হলে অর্ডার চলবে। Strict হলে নতুন app হঠাৎ ভেঙে যাবে। App store-এ ছড়িয়ে থাকা client-দের সঙ্গে কাজ করলে এই সহনশীলতা মূল্যবান।

Strict-এর পক্ষে আছে পরিষ্কার ভুল ধরা। Client লিখল { "qantity": 2 }। quantity optional হলে strip চুপচাপ বানান-ভুল ঘরটি ফেলে দিল, আর ডিফল্ট ১ বসে গেল। Request “সফল”, অর্ডার ভুল, আর কেউ জানল না। Strict সঙ্গে সঙ্গে Unrecognized key বলত। এই কারণেই API-র দুনিয়ায় Postel-এর নীতির সমালোচনাও আছে। ২০২৩ সালের RFC 9413 (Maintaining Robust Protocols) ব্যাখ্যা করে, অতিরিক্ত সহনশীলতা ভুলগুলোকে স্থায়ী করে দিতে পারে, কারণ সবাই সেই ভুলের ওপর নির্ভর করতে শুরু করে।

সিদ্ধান্ত নিন কে client. নিজের frontend আর backend একসাথে deploy হলে, অথবা খুব স্পর্শকাতর route-এ, যেমন payment বা admin, strict ভালো। ভুলটি development-এই ধরা পড়ে। অনেক বাইরের client থাকা public API-তে strip বা version করা চুক্তি বেশি ব্যবহারিক। মাঝের পথও আছে: strip করুন, কিন্তু অচেনা key log করুন, আর সংখ্যা বাড়লে দেখুন। Log-এ key-র নাম রাখুন, মান নয়, কারণ মানে ব্যক্তিগত তথ্য থাকতে পারে।

মূল উপলব্ধি: Strip আর strict ঠিক-ভুলের প্রশ্ন নয়। প্রশ্ন হলো, ভুল চুপচাপ এগিয়ে যাওয়া বেশি ক্ষতিকর, নাকি পুরোনো client ভেঙে যাওয়া।

প্র ০৪ইনপুটের সব HTML ঢোকার মুখেই মুছে দিলে তো সমস্যা শেষ। ইতিহাস এই পথকে ব্যর্থ বলে কেন?

পথটি আকর্ষণীয়, কারণ এক জায়গায় ফিল্টার বসালেই মনে হয় সব নিরাপদ। PHP ঠিক এই পরীক্ষাটি বড় আকারে করেছিল। তার magic_quotes_gpc ফিচার request-এর ইনপুটে নিজে থেকেই '-এর আগে backslash বসাত, যাতে SQL injection না হয়। ফল? ব্যবহারকারীর নাম O\'Brien হয়ে জমত, পাতায় অদ্ভুত backslash দেখা যেত, আর কোডে দ্বিগুণ escape-এর ঝামেলা হতো। সব database-এ একই নিয়মও চলে না। ফিচারটি PHP 5.3-এ deprecated হয়, আর PHP 5.4-এ সরিয়ে দেওয়া হয়। তার জায়গায় স্বীকৃত পথ হলো prepared statement.

কারণটি মৌলিক। ডেটা ঢোকার সময় জানা থাকে না সেটি কোথায় বসবে। একই লেখা HTML body-তে, HTML attribute-এ, JavaScript string-এ, URL-এ, SQL-এ আর CSV-তে ভিন্ন নিয়ম চায়। এক জায়গার নিরাপদ রূপ অন্য জায়গায় বিপজ্জনক অথবা অর্থহীন। তাই escape করতে হয় ব্যবহারের মুহূর্তে, context জেনে।

ফিল্টার ভাঙার ইতিহাসও আছে। ২০০৫ সালে MySpace-এ Samy Kamkar-এর worm একটি stored XSS ছিল। সাইটটি নানা HTML ফিল্টার করত, কিন্তু তিনি ফিল্টারের ফাঁক খুঁজে নিজের প্রোফাইলে script রাখেন। যিনি দেখতেন, তাঁর প্রোফাইলেও সেটি ছড়াত। প্রায় এক দিনের মধ্যে ১০ লাখের বেশি প্রোফাইলে ছড়ানোর কথা জানা যায়, আর সাইটটি সাময়িক বন্ধ রাখতে হয়। ফিল্টার করা মানে সব খারাপ রূপ অনুমান করা, যা অসম্ভব। Escape বা CSP সেই অনুমান চায় না।

তাহলে ঢোকার সময় কি কিছুই নেই? আছে। আকার, ধরন, দৈর্ঘ্য যাচাই। Rich text-এর ক্ষেত্রে, যেখানে সত্যিই HTML রাখতে হয়, পরীক্ষিত HTML sanitizer library. সেই ক্ষেত্রেও অনেক দল মূল লেখা জমিয়ে দেখানোর সময় sanitize করে, যাতে library হালনাগাদ হলে পুরোনো ডেটাও সুবিধা পায়।

মূল উপলব্ধি: ইনপুটে আকৃতি যাচাই, output-এ context অনুযায়ী escape. একটি “জাদুর ফিল্টার” দুই কাজের একটিও ঠিকমতো করে না।

প্র ০৫“ডেটা বৈধ” হলেও একটি request কীভাবে server নামিয়ে দিতে পারে? সীমা বসানোর পেছনে কোন ঘটনাগুলো শিক্ষা দেয়?

Denial of service-এর জন্য ভুল ডেটা লাগে না, শুধু ব্যয়বহুল ডেটা লাগে। ২০১১ সালের শেষে 28C3 সম্মেলনে Alexander Klink আর Julian Wälde দেখান, অনেক ভাষার hash table-এ একটি POST-এর হাজারো সংঘর্ষ ঘটানো key দিয়ে CPU আটকে রাখা যায়। একে HashDoS বলা হয়। প্রতিকারের একটি অংশ ছিল ঠিক এই পর্বের ভাবনা: প্যারামিটারের সংখ্যায় ছাদ। PHP-র max_input_vars সেই সময়ে এসেছে। ডেটা বৈধ ছিল, শুধু পরিমাণটিই অস্ত্র।

দ্বিতীয় ঘটনা regex-এর। ২০১৯ সালের ২ জুলাই Cloudflare-এর একটি বড় বিভ্রাটের সরকারি বিশ্লেষণে তারা জানায়, WAF-এর একটি নতুন নিয়মের regex-এর backtracking CPU প্রায় পুরোটা খেয়ে ফেলেছিল। এটি বাইরের আক্রমণ ছিল না, নিজেদের নিয়ম। Stack Overflow-ও ২০১৬ সালে একটি ফাঁকা অক্ষরের regex-এর জন্য বিভ্রাটের কথা জানিয়েছিল। শিক্ষা: যাচাইয়ের কোড নিজেও একটি খরচ।

এই পর্বের মাপ একই গল্প বলে। .max ছাড়া ৫ MB-র ইনপুট ৩৮ MB ভুলের তালিকা বানাল। Attacker-এর একটি byte-এর বিনিময়ে যদি server অনেক গুণ কাজ করে, তাকে বলে amplification. ভালো নকশা এই অনুপাত ১-এর কাছে রাখে। তাই সীমাগুলো ঢালের মতো স্তরে বসে: reverse proxy-র body limit, express.json-এর limit, schema-র .max, database-এর statement timeout, আর rate limit. কেউ একা যথেষ্ট নয়।

সীমা বসানোরও একটি ঝুঁকি আছে। খুব ছোট সীমা বৈধ ব্যবহারকারীকে আটকায়। ১৮০০ byte-এর order-এ ২ KB সীমা দিলে পরে note-এর দৈর্ঘ্য বাড়াতে গিয়ে আচমকা 413 আসবে। তাই সীমা হিসাব করে, একটু জায়গা রেখে ঠিক করুন, আর 413 আর 422-এর হার নজরে রাখুন। হঠাৎ বাড়লে হয় আক্রমণ, নয় নিজের client-এর বাগ।

মূল উপলব্ধি: যাচাই শুধু “এটি ঠিক কি না” প্রশ্ন নয়, “এটি কত খরচ করাতে পারে” প্রশ্নও। প্রতিটি ইনপুটের ওপরের সীমা না থাকলে নিরাপত্তা অসম্পূর্ণ।

অনুশীলন

হিসাব, কমান্ড, অনুমান আর সমালোচনা, চারটিই আছে।

  1. সীমার হিসাব: Review-এর schema: rating ১–৫, text সর্বোচ্চ ২,০০০ অক্ষর। লেখা পুরোটা বাংলা, প্রতি অক্ষর ৩ byte. JSON-এর কাঠামো {"rating":5,"text":""} ২২ byte. (ক) সবচেয়ে বড় বৈধ body কত byte? (খ) ১০০kb (১,০২,৪০০ byte) সীমা এর কত গুণ? (গ) ২,৫০,০০০ সারির টেবিলে প্রতি সারি ৪০০ byte হলে সীমাহীন তালিকার উত্তর কত, আর limit ১০০ হলে কত? (ঘ) page=1000&limit=100-এর offset কত?

    (ক) ২,০০০ × ৩ + ২২ = ৬,০২২ byte. (খ) ১,০২,৪০০ ÷ ৬,০২২ ≈ ১৭ গুণ, তাই এই route-এ 8kb (৮,১৯২ byte) সীমা যথেষ্ট। (গ) ২,৫০,০০০ × ৪০০ = ১০,০০,০০,০০০ byte, প্রায় ১০০ MB; ১০০ × ৪০০ = ৪০,০০০ byte, প্রায় ৪০ KB, পার্থক্য ২,৫০০ গুণ। (ঘ) (১০০০ − ১) × ১০০ = ৯৯,৯০০.

  2. নিজে চালিয়ে দেখুন: Express 5 আর Zod 4-এ validate, listQuery আর orderBody বসান। তারপর curl দিয়ে পাঠান: (ক) /api/products?limit=1000, (খ) /api/products/abc, (গ) {bad body, (ঘ) content-type: text/plain, (ঙ) ৫ MB-র array. প্রতিটিতে কোন status আশা করবেন, আর কোনটিতে কী দেখলে বাগ?

    এই পর্বের সত্যিকারের run-এ: (ক) 422, path: limit. (খ) 422, in: params. (গ) 400, BAD_JSON. (ঘ) 415, JSON_REQUIRED. (ঙ) 413, TOO_LARGE, তবে সীমা ১০০kb থাকলে। বাগের লক্ষণ: 500, HTML পাতা, stack trace, উত্তরে ছাঁচের অমিল, অথবা 413-এর বদলে দীর্ঘ অপেক্ষা। শেষ করে নিজের server শুধু নিজের process ID দিয়ে বন্ধ করুন।

  3. আউটপুট অনুমান: Zod 4 আর Express 5. inStock: z.coerce.boolean(), minPrice: z.coerce.number().min(0), limit: z.coerce.number().int().max(100), page: z.coerce.number().int(). URL-গুলো: (ক) ?inStock=false, (খ) ?minPrice=, (গ) ?limit=1e1, (ঘ) ?page=1&page=2. প্রতিটিতে কী ফল?

    (ক) true, কারণ অক্ষর থাকা string Boolean-এ সত্য। (খ) 0, কারণ Number('') ০, আর ফিল্টার ভুলভাবে “০ থেকে” হলো। (গ) 10, সত্যিকারের run-এ "limit":10. (ঘ) ভুল, expected number, received NaN, কারণ Express array বানায়, আর দুই উপাদানের array Number-এ NaN. (ক), (খ) সত্যিকারের ফাঁদ; সমাধান z.stringbool(), আর খালি string আগে undefined-এ বদলানো।

  4. AI-এর কোডের সমালোচনা: এক AI লিখল: app.patch('/api/products/:id', async (req, res) => { const d = schema.parse(req.body); req.query = schema2.parse(req.query); const p = await db.update({ where: { id: req.params.id }, data: req.body }); await db.query(`SELECT … ORDER BY ${req.query.sort}`); res.json(p); });. কমপক্ষে পাঁচটি সমস্যা আর সমাধান বলুন।
    • parse ভুলে throw করে, handler ছাড়া 500 হতে পারে। validate middleware আর safeParse দিন।
    • req.query = Express 5-এ TypeError. res.locals ব্যবহার করুন।
    • data: req.body: যাচাই করা d ব্যবহারই হয়নি, mass assignment. ফল ব্যবহার করুন, ঘর নাম ধরে দিন।
    • req.params.id string আর যাচাই ছাড়া। z.coerce.number() দিয়ে params যাচাই করুন।
    • ORDER BY ${…}: SQL-এ সরাসরি ইনপুট। z.enum আর column-এর তালিকা দিন।
    • কে সম্পাদনা করতে পারেন সেই permission নেই। পর্ব ৪৮-এর স্তর যোগ করুন, আর res.json(p)-এর ঘরগুলোও বেছে নিন।

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

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

ক্রমExpress API-তে একটি POST request-এর ফটকগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. express.json আকারের সীমা দেখে body পড়ে
  2. validate schema দিয়ে আকৃতি, type আর সীমা যাচাই করে
  3. Handler শুধু যাচাই করা ফল নেয়
  4. Service মজুতের মতো ব্যবসার নিয়ম দেখে
  5. Repository parameterized query চালায়
  6. পাতায় দেখানোর সময় লেখা escape হয়

Zod 4 আর Express-এ z.coerce.boolean() দিয়ে query যাচাই করা হলো। URL-এ ?inStock=false এলে ফল কী?

  • false
  • true
  • একটি validation ভুল
  • undefined

Express 5-এর একটি ES module-এ middleware লিখল req.query = parsed. কী ঘটবে?

  • req.query নতুন মান পায়
  • মানটি নিজে থেকে res.locals-এ চলে যায়
  • TypeError: property-টির শুধু getter আছে
  • পরের request-এর query বদলে যায়

একটি PATCH route-এ validate({ body: profileBody }) আছে, আর profileBody z.object দিয়ে বানানো, যাতে role নেই। Handler-এ লেখা Object.assign(user, req.body). Client role: 'admin' পাঠালে কী হবে?

  • role বদলে যাবে, কারণ handler মূল req.body ব্যবহার করছে
  • Zod req.body থেকেই key মুছে দিয়েছে, তাই নিরাপদ
  • 422 ফিরবে, কারণ অচেনা key আছে
  • Object.assign অচেনা key উপেক্ষা করবে

বাজারঘরে একটি পণ্যের মজুত ২টি, আর ক্রেতা ৩টি চাইলেন। qty-র বৈধ সীমা ১ থেকে ১০। এই নিয়ম কোথায় থাকা উচিত, আর উত্তর কী?

  • Zod schema-য় async refine, 422
  • React form-এ, কোনো server যাচাই ছাড়া
  • express.json-এ, 400
  • Service-এ, 409

৫ MB-র একটি JSON body এলো, আর express.json({ limit: '100kb' }) বসানো। Schema-তে z.array(…).max(20)-ও আছে। কে আটকাবে, আর সবচেয়ে বড় লাভ কী?

  • .max(20), 422, JSON parse হওয়ার পরে
  • express.json, 413, JSON.parse চলার আগেই
  • Handler, 500
  • Service, 409

একটি review-এ লেখা আছে দাম <১০০ টাকা <script>…</script>. এটি একটি সাধারণ HTML পাতায় দেখানো হবে। সবচেয়ে ঠিক সুরক্ষা কোনটি?

  • ঢোকার সময় সব < মুছে ফেলা
  • শুধু .max(2000) দেওয়া
  • পাতায় বসানোর সময় HTML escape করা
  • Parameterized query ব্যবহার করা

শব্দকোষ

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

মনে রাখুন

  1. চার অংশই অচেনা: params, query, headers, body. validate সবগুলো যাচাই করে, ফল রাখে res.locals-এ, আর handler শুধু সেটিই পড়ে।
  2. Coerce শুধু URL-এ: "" হয় ০, "false" হয় true, তাই boolean-এ z.stringbool().
  3. অজানা key ফলে না ওঠাই mass assignment-এর ওষুধ, আর প্রতিটি ইনপুটে ওপরের সীমা: byte, array, string, limit.
  4. এক ছাঁচ: 400 পড়া যায়নি, 415 ভুল ধরন, 413 বড়, 422 নিয়ম ভাঙা, 409 অবস্থার সংঘাত। Schema থেকে আসে type.
  5. আকৃতি schema-য়, অবস্থা service-এ: ঢোকার সময় যাচাই, বের হওয়ার সময় escape, আর SQL-এ সব সময় parameter.

পরের পর্বে: ভুল ডেটা তো আটকানো গেল, কিন্তু কোড নিজে ভাঙলে? পর্ব ২৮-এ async error, central error handler আর ব্যবহারকারীকে নিরাপদ বার্তা দেওয়ার পথ।

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

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