Input Validation (ইনপুট ভ্যালিডেশন)
Input Validation কী?
Input Validation হলো user-এর পাঠানো data গ্রহণ করার আগে যাচাই করা — data সঠিক format-এ আছে কিনা, নিরাপদ কিনা, business rule মানছে কিনা।
User Input → Validation → ✅ Valid → Process → Database
→ ❌ Invalid → Error Response (details সহ)Golden Rule: Never trust user input. User হোক বা API consumer — বাইরে থেকে আসা যেকোনো data potentially dangerous বা ভুল।
কেন Validation দরকার?
Validation ছাড়া কী হতে পারে?
// ❌ No validation
app.post("/api/users", async (req, res) => {
const user = await User.create(req.body); // যা আসবে তাই save!
res.json(user);
});সমস্যা ১ — Invalid Data:
{ email: "not-an-email", age: -5, name: "" }
→ Database-এ garbage data ঢুকে যাবে
সমস্যা ২ — Security Attack:
{ email: "admin@test.com", role: "admin" }
→ User নিজেকে admin বানিয়ে দিলো!
সমস্যা ৩ — SQL Injection:
{ name: "'; DROP TABLE users; --" }
→ পুরো table delete!
সমস্যা ৪ — Server Crash:
{ data: "a".repeat(100000000) }
→ Memory overflow → Server crashValidation থাকলে
// ✅ With validation
app.post("/api/users", validate(createUserSchema), async (req, res) => {
const user = await User.create(req.body);
res.json(user);
});
// ভুল input পাঠালে:
// 400 Bad Request
// {
// "error": {
// "code": "VALIDATION_ERROR",
// "details": [
// { "field": "email", "message": "Must be a valid email" },
// { "field": "age", "message": "Must be at least 0" }
// ]
// }
// }Validation কোথায় করবে?
Layer কী Check করবে
────────────────────────────────────────────────────────
Client-side UX improvement (instant feedback)
⚠️ Bypass করা যায় — trust করো না!
API Layer Input format, type, required fields
(Controller) → এখানেই মূল validation
Service Layer Business rules, cross-field logic
(Business Logic) → "User 3টার বেশি order দিতে পারবে না"
Database Layer Constraints, unique, not null
(Schema) → Last line of defense❌ শুধু Client-side validation = NO validation
(DevTools দিয়ে bypass করা যায়)
✅ Server-side validation = MUST
Client-side validation = UX bonus (optional)Validation-এর ধরন
1. Type Validation
// Data সঠিক type-এ আছে কিনা
typeof name === "string"; // String check
typeof age === "number"; // Number check
Array.isArray(tags); // Array check
typeof isActive === "boolean"; // Boolean check2. Format Validation
// নির্দিষ্ট format মানছে কিনা
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) // Email
/^01[3-9]\d{8}$/.test(phone) // BD phone
/^\d{4}-\d{2}-\d{2}$/.test(date) // Date (YYYY-MM-DD)
/^https?:\/\/.+/.test(url) // URL3. Range/Length Validation
name.length >= 2 && name.length <= 100; // String length
age >= 0 && age <= 150; // Number range
password.length >= 8; // Minimum length
tags.length <= 10; // Array max items
fileSize <= 5 * 1024 * 1024; // Max 5MB4. Required/Optional
// Required field আছে কিনা
if (!email) throw new Error("Email is required");
if (!password) throw new Error("Password is required");
// Optional field — থাকলে validate, না থাকলে skip
if (phone && !isValidPhone(phone)) {
throw new Error("Invalid phone number");
}5. Business Rule Validation
// Application-specific rules
if (startDate >= endDate) throw new Error("Start must be before end");
if (quantity > stock) throw new Error("Not enough stock");
if (user.orders.length >= 3) throw new Error("Max 3 pending orders");6. Whitelist Validation
// শুধু allowed values accept করো
const ROLES = ["user", "moderator", "admin"];
if (!ROLES.includes(role)) throw new Error("Invalid role");
const SORT_FIELDS = ["name", "email", "createdAt"];
if (!SORT_FIELDS.includes(sortBy)) throw new Error("Invalid sort field");Joi (Node.js Validation Library)
Installation
npm install joiBasic Schema
const Joi = require("joi");
const createUserSchema = Joi.object({
name: Joi.string().min(2).max(100).required().messages({
"string.min": "নাম কমপক্ষে ২ অক্ষরের হতে হবে",
"any.required": "নাম আবশ্যক",
}),
email: Joi.string().email().required(),
password: Joi.string()
.min(8)
.max(128)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.required()
.messages({
"string.pattern.base":
"Password-এ uppercase, lowercase ও number থাকতে হবে",
}),
age: Joi.number().integer().min(13).max(150).optional(),
role: Joi.string().valid("user", "moderator").default("user"),
tags: Joi.array().items(Joi.string().max(30)).max(10).optional(),
address: Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
zip: Joi.string()
.pattern(/^\d{4}$/)
.required(),
}).optional(),
});Schema ব্যবহার
// Direct validation
const { error, value } = createUserSchema.validate(req.body, {
abortEarly: false, // সব error একসাথে দেখাও
stripUnknown: true, // schema-তে নেই এমন field বাদ দাও
});
if (error) {
const details = error.details.map((d) => ({
field: d.path.join("."),
message: d.message,
}));
return res.status(400).json({
status: "error",
error: { code: "VALIDATION_ERROR", message: "Validation failed", details },
});
}
// value = validated + sanitized data (unknown fields stripped)Validation Middleware (Reusable)
function validate(schema, source = "body") {
return (req, res, next) => {
const { error, value } = schema.validate(req[source], {
abortEarly: false,
stripUnknown: true,
});
if (error) {
const details = error.details.map((d) => ({
field: d.path.join("."),
message: d.message,
}));
return res.status(400).json({
status: "error",
error: {
code: "VALIDATION_ERROR",
message: "Validation failed",
details,
},
});
}
req[source] = value;
next();
};
}
// Usage:
app.post("/api/users", validate(createUserSchema), createUser);
app.get("/api/users", validate(querySchema, "query"), getUsers);
app.put(
"/api/users/:id",
validate(paramsSchema, "params"),
validate(updateUserSchema),
updateUser,
);Advanced Joi Features
// Conditional Validation
const schema = Joi.object({
type: Joi.string().valid("personal", "business").required(),
// type = 'business' হলে companyName required
companyName: Joi.when("type", {
is: "business",
then: Joi.string().required(),
otherwise: Joi.forbidden(),
}),
// passwordConfirm must match password
password: Joi.string().min(8).required(),
passwordConfirm: Joi.string()
.valid(Joi.ref("password"))
.required()
.messages({ "any.only": "Passwords must match" }),
});
// Custom Validation
const schema = Joi.object({
phone: Joi.string()
.custom((value, helpers) => {
if (!value.startsWith("01") || value.length !== 11) {
return helpers.error("any.invalid");
}
return value;
})
.messages({
"any.invalid": "Valid Bangladeshi phone number required (01XXXXXXXXX)",
}),
});
// Rename / Transform
const schema = Joi.object({
first_name: Joi.string().required(),
last_name: Joi.string().required(),
})
.rename("firstName", "first_name")
.rename("lastName", "last_name");Zod (TypeScript-First Validation)
TypeScript project-এ Zod বেশি popular — schema থেকে type auto-generate হয়:
Installation
npm install zodBasic Schema
import { z } from "zod";
const createUserSchema = z.object({
name: z.string().min(2, "নাম কমপক্ষে ২ অক্ষরের হতে হবে").max(100),
email: z.string().email("Valid email দিন"),
password: z
.string()
.min(8, "Password কমপক্ষে ৮ অক্ষরের হতে হবে")
.regex(/[A-Z]/, "কমপক্ষে একটি uppercase letter")
.regex(/[0-9]/, "কমপক্ষে একটি number"),
age: z.number().int().min(13).max(150).optional(),
role: z.enum(["user", "moderator"]).default("user"),
tags: z.array(z.string().max(30)).max(10).optional(),
address: z
.object({
street: z.string(),
city: z.string(),
zip: z.string().regex(/^\d{4}$/),
})
.optional(),
});
// Schema থেকে Type auto-generate!
type CreateUserInput = z.infer<typeof createUserSchema>;
// {
// name: string;
// email: string;
// password: string;
// age?: number;
// role: 'user' | 'moderator';
// tags?: string[];
// address?: { street: string; city: string; zip: string };
// }Zod Validation
// Parse (throws on error)
try {
const data = createUserSchema.parse(req.body);
} catch (error) {
if (error instanceof z.ZodError) {
const details = error.errors.map((e) => ({
field: e.path.join("."),
message: e.message,
}));
return res.status(400).json({
status: "error",
error: { code: "VALIDATION_ERROR", details },
});
}
}
// SafeParse (doesn't throw)
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
const details = result.error.errors.map((e) => ({
field: e.path.join("."),
message: e.message,
}));
return res.status(400).json({
status: "error",
error: { code: "VALIDATION_ERROR", details },
});
}
const validData = result.data; // TypeScript type-safe!Zod Middleware
function validate(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
status: "error",
error: {
code: "VALIDATION_ERROR",
message: "Validation failed",
details: result.error.errors.map((e) => ({
field: e.path.join("."),
message: e.message,
})),
},
});
}
req.body = result.data;
next();
};
}
app.post("/api/users", validate(createUserSchema), createUser);Advanced Zod Features
// Transform
const schema = z.string().transform((val) => val.trim().toLowerCase());
schema.parse(" HELLO "); // "hello"
// Refine (custom validation)
const passwordSchema = z
.object({
password: z.string().min(8),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords must match",
path: ["confirmPassword"],
});
// Union (or)
const idSchema = z.union([z.string().uuid(), z.number().int().positive()]);
// Discriminated Union
const eventSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
z.object({ type: z.literal("scroll"), offset: z.number() }),
z.object({ type: z.literal("keypress"), key: z.string() }),
]);
// Partial (সব field optional)
const updateUserSchema = createUserSchema.partial();
// Pick / Omit
const loginSchema = createUserSchema.pick({ email: true, password: true });
const publicUserSchema = createUserSchema.omit({ password: true });Joi vs Zod Comparison
Feature Joi Zod
────────────────────────────────────────────────────────
Language JavaScript TypeScript-first
Type Inference ❌ Manual ✅ Auto (z.infer)
Bundle Size ~150KB ~50KB
Custom Messages ✅ .messages() ✅ Inline string
Conditional ✅ .when() ✅ .refine()
Transform ❌ Limited ✅ .transform()
Ecosystem Express/Hapi tRPC, Next.js, React Hook Form
Best For JS projects TS projectsPython — Pydantic
Python-এ সবচেয়ে popular validation library। FastAPI-এর সাথে built-in:
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional
from enum import Enum
class Role(str, Enum):
user = "user"
moderator = "moderator"
admin = "admin"
class CreateUserRequest(BaseModel):
name: str = Field(min_length=2, max_length=100)
email: EmailStr
password: str = Field(min_length=8, max_length=128)
age: Optional[int] = Field(None, ge=13, le=150)
role: Role = Role.user
tags: Optional[list[str]] = Field(None, max_length=10)
@validator('password')
def password_strength(cls, v):
if not any(c.isupper() for c in v):
raise ValueError('Must contain uppercase letter')
if not any(c.isdigit() for c in v):
raise ValueError('Must contain a digit')
return v
@validator('name')
def name_must_not_be_empty(cls, v):
if not v.strip():
raise ValueError('Name cannot be empty')
return v.strip()
class Config:
extra = "forbid" # unknown fields reject করোFastAPI-তে ব্যবহার
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.post("/api/users")
async def create_user(user: CreateUserRequest):
# FastAPI automatically validate করে!
# ভুল data আসলে 422 Unprocessable Entity return করে
return {"message": "User created", "data": user.dict()}// ভুল input-এ FastAPI auto response:
{
"detail": [
{
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"type": "value_error.email"
},
{
"loc": ["body", "password"],
"msg": "Must contain uppercase letter",
"type": "value_error"
}
]
}Input Sanitization
Validation = data format ঠিক আছে কিনা check। Sanitization = data clean করা — dangerous parts বাদ দেওয়া।
Validation → "এই data কি acceptable?"
Sanitization → "এই data-কে safe বানাও"
দুটোই দরকার — আগে validate, তারপর sanitize।Common Sanitization
// Trim whitespace
const name = input.name.trim();
// Lowercase email
const email = input.email.toLowerCase().trim();
// Remove HTML tags (XSS prevention)
const sanitizeHtml = require("sanitize-html");
const cleanContent = sanitizeHtml(input.content, {
allowedTags: ["b", "i", "em", "strong", "p", "br"],
allowedAttributes: {},
});
// Escape special characters
const escapeHtml = require("escape-html");
const safeString = escapeHtml(input.comment);
// "<script>alert('xss')</script>"
// → "<script>alert('xss')</script>"SQL Injection Prevention
// ❌ String concatenation — SQL Injection vulnerable!
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Input: email = "' OR 1=1 --"
// Query becomes: SELECT * FROM users WHERE email = '' OR 1=1 --'
// → সব users return!
// ✅ Parameterized Queries
const result = await db.query("SELECT * FROM users WHERE email = $1", [email]);
// ✅ ORM ব্যবহার করো (Prisma, Sequelize, Mongoose)
const user = await User.findOne({ where: { email } });NoSQL Injection Prevention
// ❌ Vulnerable — user object পাঠাতে পারে
app.post("/login", async (req, res) => {
const user = await User.findOne({
email: req.body.email,
password: req.body.password,
});
});
// Input: { "password": { "$gt": "" } }
// → Password check bypass!
// ✅ Type check + sanitize
app.post("/login", async (req, res) => {
if (
typeof req.body.email !== "string" ||
typeof req.body.password !== "string"
) {
return res.status(400).json({ error: "Invalid input type" });
}
const user = await User.findOne({ email: req.body.email });
const isMatch = await bcrypt.compare(req.body.password, user.password);
});express-validator
npm install express-validatorconst { body, param, query, validationResult } = require("express-validator");
app.post(
"/api/users",
[
body("name")
.trim()
.notEmpty()
.withMessage("Name is required")
.isLength({ min: 2, max: 100 }),
body("email")
.isEmail()
.withMessage("Valid email required")
.normalizeEmail(),
body("password")
.isLength({ min: 8 })
.matches(/[A-Z]/)
.withMessage("Must contain uppercase")
.matches(/[0-9]/)
.withMessage("Must contain number"),
body("age").optional().isInt({ min: 13, max: 150 }),
body("role").optional().isIn(["user", "moderator"]),
],
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
status: "error",
error: {
code: "VALIDATION_ERROR",
details: errors.array().map((e) => ({
field: e.path,
message: e.msg,
})),
},
});
}
next();
},
createUser,
);Validation Strategy — কোথায় কী Validate করবে?
Request Params
const paramsSchema = Joi.object({
id: Joi.string()
.pattern(/^[0-9a-fA-F]{24}$/)
.required(),
// MongoDB ObjectId format
});
app.get("/api/users/:id", validate(paramsSchema, "params"), getUser);Query Parameters
const querySchema = Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(20),
sort: Joi.string().valid("name", "email", "createdAt").default("createdAt"),
order: Joi.string().valid("asc", "desc").default("desc"),
search: Joi.string().max(200).optional(),
});
app.get("/api/users", validate(querySchema, "query"), getUsers);Request Body
const createUserSchema = Joi.object({
name: Joi.string().trim().min(2).max(100).required(),
email: Joi.string().email().lowercase().required(),
password: Joi.string().min(8).max(128).required(),
});
const updateUserSchema = Joi.object({
name: Joi.string().trim().min(2).max(100),
email: Joi.string().email().lowercase(),
age: Joi.number().integer().min(13).max(150),
}).min(1); // কমপক্ষে একটা field থাকতে হবে
app.post("/api/users", validate(createUserSchema), createUser);
app.patch("/api/users/:id", validate(updateUserSchema), updateUser);Headers
const headerSchema = Joi.object({
authorization: Joi.string()
.pattern(/^Bearer .+/)
.required(),
"content-type": Joi.string().valid("application/json").required(),
}).unknown(true); // অন্যান্য headers allow করোFile Uploads
function validateFile(req, res, next) {
const file = req.file;
if (!file) {
return res.status(400).json({ error: "File is required" });
}
const allowedTypes = ["image/jpeg", "image/png", "image/webp"];
if (!allowedTypes.includes(file.mimetype)) {
return res.status(400).json({ error: "Only JPEG, PNG, WebP allowed" });
}
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
return res.status(400).json({ error: "File size must be under 5MB" });
}
next();
}Validation Error Response — Best Practices
// ✅ Good — specific field-level errors
{
"status": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "password", "message": "Must be at least 8 characters" },
{ "field": "age", "message": "Must be between 13 and 150" }
]
}
}
// ❌ Bad — vague error
{ "error": "Bad request" }
// ❌ Bad — only first error (abortEarly: true)
{ "error": "Email is invalid" }
// User fix করে আবার submit → নতুন error → frustrating!Best Practices:
✅ সব error একসাথে দেখাও (abortEarly: false)
✅ প্রতিটি field-এর error আলাদা
✅ Human-readable message
✅ Machine-readable field name
✅ 400 status code (client error)সংক্ষেপে মনে রাখার সূত্র
Input Validation = বাইরে থেকে আসা data যাচাই
Input Sanitization = data safe/clean করা
দুটোই দরকার!
Never Trust User Input — NEVER.
Validation Layers:
Client-side → UX only, bypass করা যায়
API Layer → MUST (Joi/Zod/Pydantic)
Service → Business rules
Database → Constraints (last defense)
Validation Types:
Type → string, number, boolean
Format → email, phone, date
Range → min, max, length
Required → present or not
Business → custom rules
Whitelist → allowed values only
Popular Libraries:
JavaScript → Joi, express-validator
TypeScript → Zod (auto type inference!)
Python → Pydantic (FastAPI built-in)
Sanitization:
trim(), lowercase(), escape HTML
Parameterized queries (SQL Injection prevention)
Type checking (NoSQL Injection prevention)
Error Response:
400 status code
abortEarly: false → সব error একসাথে
field + message per errorInterview Golden Lines
Never trust user input — always validate on the server side. Client-side validation is for UX, not security.
Validation checks if data is correct; Sanitization makes data safe. Both are required.
Zod provides TypeScript type inference from schemas — one source of truth for validation and types.
Always use parameterized queries to prevent SQL injection — never concatenate user input into queries.
Return all validation errors at once (abortEarly: false) with field-level details for a good API consumer experience.