Input Validation & Data Sanitization with Joi or Zod0%

Input Validation & Data Sanitization with Joi or Zod

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Input Validation & Data Sanitization with Joi or Zod

Never trust client input. Accepting unsanitized, unvalidated data directly into database queries or business operations leads to SQL/NoSQL injection, cross-site scripting (XSS), prototype pollution, and application crashes. In modern Node.js and TypeScript architectures, Zod has emerged as the premier schema validation library.


1. Why Zod?

  • TypeScript-First: Static type inference is derived directly from the runtime schema (z.infer<typeof schema>), eliminating duplicate type declarations.
  • Zero Dependencies: Extremely fast, lightweight, and works seamlessly on both Node.js backends and browser frontends.
  • Composable & Declarative: Intuitive chainable validators for strings, numbers, arrays, and nested objects.

2. Defining a Zod Validation Schema

JavaScript
import { z } from 'zod';
 
export const registerUserSchema = z.object({
body: z.object({
username: z
.string({ required_error: 'Username is required' })
.min(3, 'Username must be at least 3 characters')
.max(30, 'Username cannot exceed 30 characters')
.regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),
email: z
.string({ required_error: 'Email is required' })
.email('Invalid email address format')
.toLowerCase()
.trim(),
 
password: z
.string({ required_error: 'Password is required' })
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[0-9]/, 'Password must contain at least one number'),
 
age: z
.number()
.int()
.min(18, 'Must be at least 18 years old')
.optional()
})
});

3. Creating a Reusable Validation Middleware

JavaScript
// src/middleware/validate.js
export const validate = (schema) => async (req, res, next) => {
try {
// Validate params, query, and body against the schema
const parsed = await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params
});
 
// Replace req properties with clean, sanitized, coerced data
req.body = parsed.body;
req.query = parsed.query;
req.params = parsed.params;
 
next();
} catch (error) {
if (error.name === 'ZodError') {
const formattedErrors = error.errors.map(err => ({
field: err.path.join('.').replace('body.', ''),
message: err.message
}));
 
return res.status(400).json({
status: 'fail',
message: 'Validation failed',
errors: formattedErrors
});
}
next(error);
}
};

4. Applying Validation to Routes

JavaScript
import { Router } from 'express';
import { validate } from '../middleware/validate.js';
import { registerUserSchema } from '../schemas/user.schema.js';
import { registerUser } from '../controllers/auth.controller.js';
 
const router = Router();
 
// Route is protected: invalid payloads are rejected BEFORE hitting the controller!
router.post('/register', validate(registerUserSchema), registerUser);
 
export default router;

Interactive Quiz

Multiple Choice Questions

Question 1
What is the primary advantage of validating request input with a library like Zod or Joi before hitting the controller?
Question 2
How can TypeScript types be inferred directly from a Zod schema without writing manual interfaces?
Question 3
Which HTTP status code is standard when a request payload fails schema validation rules?
Question 4
What does the .trim() and .toLowerCase() transformers on a Zod string accomplish?
Question 5
What method on a Zod schema validates data asynchronously and throws a ZodError if validation fails?

Next Lesson

User Authentication with JWT (JSON Web Tokens) & HTTP-Only Cookies

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext