Node.js & Express

Request Validation and Sanitization

30 min Lesson 12 of 40

Request Validation and Sanitization

Validating and sanitizing user input is critical for building secure and reliable APIs. Validation ensures data meets expected requirements, while sanitization cleans data to prevent security vulnerabilities like XSS and SQL injection.

Why Validation and Sanitization?

  • Security: Prevent injection attacks (SQL, XSS, NoSQL)
  • Data Integrity: Ensure data matches expected format and type
  • User Experience: Provide clear error messages
  • Business Logic: Enforce business rules and constraints
  • Database Consistency: Prevent invalid data from entering database
Warning: Never trust user input. Always validate and sanitize data on the server-side, even if you have client-side validation.

Express-Validator

Express-validator is a popular validation and sanitization library built on validator.js:

# Install express-validator npm install express-validator

Basic Validation Example

Let's validate user registration data:

const express = require('express'); const { body, validationResult } = require('express-validator'); const app = express(); app.use(express.json()); // POST /api/users - Create user with validation app.post('/api/users', // Validation rules [ body('username') .trim() .isLength({ min: 3, max: 20 }) .withMessage('Username must be 3-20 characters') .isAlphanumeric() .withMessage('Username must contain only letters and numbers'), body('email') .trim() .isEmail() .withMessage('Invalid email address') .normalizeEmail(), body('password') .isLength({ min: 8 }) .withMessage('Password must be at least 8 characters') .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/) .withMessage('Password must contain uppercase, lowercase, number, and special character'), body('age') .optional() .isInt({ min: 13, max: 120 }) .withMessage('Age must be between 13 and 120'), body('website') .optional() .isURL() .withMessage('Invalid URL format') ], // Handler (req, res) => { // Check validation results const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ success: false, message: 'Validation failed', errors: errors.array() }); } // Validation passed, proceed with user creation const { username, email, password, age, website } = req.body; res.status(201).json({ success: true, message: 'User created successfully', data: { username, email } }); } ); app.listen(3000);

Common Validation Methods

Express-validator provides numerous validation methods:

// String validators .isLength({ min: 5, max: 100 }) // Length check .isEmpty() // Empty check .isAlpha() // Letters only .isAlphanumeric() // Letters and numbers .isNumeric() // Numbers only .isEmail() // Email format .isURL() // URL format .isIP() // IP address .isJSON() // Valid JSON .matches(/regex/) // Custom regex // Number validators .isInt({ min: 0, max: 100 }) // Integer range .isFloat({ min: 0.0, max: 1.0 }) // Float range .isDecimal() // Decimal number // Boolean validators .isBoolean() // Boolean value // Date validators .isDate() // Valid date .isISO8601() // ISO date format .isBefore('2025-12-31') // Date before .isAfter('2020-01-01') // Date after // Special validators .isUUID() // UUID format .isJWT() // JWT token .isCreditCard() // Credit card number .isPostalCode('US') // Postal code .isMobilePhone('en-US') // Phone number

Sanitization Methods

Sanitization cleans and transforms input data:

// String sanitization .trim() // Remove whitespace .escape() // HTML escape (< > & ' " /) .unescape() // HTML unescape .toLowerCase() // Convert to lowercase .toUpperCase() // Convert to uppercase .normalizeEmail() // Normalize email (lowercase, remove dots from Gmail) .blacklist('chars') // Remove blacklisted characters .whitelist('chars') // Keep only whitelisted characters // Number sanitization .toInt() // Convert to integer .toFloat() // Convert to float .toBoolean() // Convert to boolean // Date sanitization .toDate() // Convert to Date object // Custom sanitization .customSanitizer(value => { return value.replace(/[^a-zA-Z0-9]/g, ''); })
Tip: Chain sanitization before validation. For example, use .trim() before .isLength() to avoid counting whitespace.

Custom Validators

Create custom validation logic for specific requirements:

const { body, validationResult } = require('express-validator'); // Custom validator example app.post('/api/users', [ body('username') .custom(async (value) => { // Check if username already exists in database const user = await User.findOne({ username: value }); if (user) { throw new Error('Username already exists'); } return true; }), body('password') .custom((value, { req }) => { // Access other fields using req if (value === req.body.username) { throw new Error('Password cannot be same as username'); } return true; }), body('passwordConfirm') .custom((value, { req }) => { if (value !== req.body.password) { throw new Error('Passwords do not match'); } return true; }), body('birthDate') .custom((value) => { const age = calculateAge(value); if (age < 18) { throw new Error('Must be at least 18 years old'); } return true; }) ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ errors: errors.array() }); } // Proceed... } ); function calculateAge(birthDate) { const today = new Date(); const birth = new Date(birthDate); let age = today.getFullYear() - birth.getFullYear(); const monthDiff = today.getMonth() - birth.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) { age--; } return age; }

Validating Arrays and Nested Objects

Validate complex data structures:

const { body, validationResult } = require('express-validator'); // Validate array of objects app.post('/api/orders', [ // Validate array body('items') .isArray({ min: 1 }) .withMessage('Order must contain at least one item'), // Validate each array element body('items.*.productId') .isInt() .withMessage('Invalid product ID'), body('items.*.quantity') .isInt({ min: 1, max: 100 }) .withMessage('Quantity must be between 1 and 100'), body('items.*.price') .isFloat({ min: 0.01 }) .withMessage('Price must be positive'), // Validate nested object body('shippingAddress.street') .trim() .notEmpty() .withMessage('Street is required'), body('shippingAddress.city') .trim() .notEmpty() .withMessage('City is required'), body('shippingAddress.zipCode') .isPostalCode('US') .withMessage('Invalid ZIP code') ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ errors: errors.array() }); } // Process order... } ); // Example request body: // { // "items": [ // { "productId": 1, "quantity": 2, "price": 29.99 }, // { "productId": 5, "quantity": 1, "price": 49.99 } // ], // "shippingAddress": { // "street": "123 Main St", // "city": "New York", // "zipCode": "10001" // } // }

Validation with Joi

Joi is another popular validation library with a different approach:

# Install Joi npm install joi
const Joi = require('joi'); // Define validation schema const userSchema = Joi.object({ username: Joi.string() .alphanum() .min(3) .max(20) .required(), email: Joi.string() .email() .required(), password: Joi.string() .min(8) .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/) .required() .messages({ 'string.pattern.base': 'Password must contain uppercase, lowercase, number, and special character' }), age: Joi.number() .integer() .min(13) .max(120) .optional(), birthDate: Joi.date() .max('now') .required(), website: Joi.string() .uri() .optional(), role: Joi.string() .valid('user', 'admin', 'moderator') .default('user'), preferences: Joi.object({ newsletter: Joi.boolean().default(false), notifications: Joi.boolean().default(true) }).optional() }); // Middleware to validate with Joi const validateUser = (req, res, next) => { const { error, value } = userSchema.validate(req.body, { abortEarly: false, // Return all errors, not just first stripUnknown: true // Remove unknown fields }); if (error) { const errors = error.details.map(detail => ({ field: detail.path.join('.'), message: detail.message })); return res.status(422).json({ success: false, message: 'Validation failed', errors }); } // Replace req.body with validated and sanitized data req.body = value; next(); }; // Use middleware app.post('/api/users', validateUser, (req, res) => { // req.body is now validated and sanitized res.status(201).json({ success: true, data: req.body }); });
Note: Joi is more declarative and provides schema reusability, while express-validator integrates directly with Express routes. Choose based on your preference and project needs.

Creating Reusable Validation Middleware

Organize validation logic in reusable middleware:

// validators/userValidator.js const { body, param, validationResult } = require('express-validator'); // Validation rules const userValidationRules = () => { return [ body('username') .trim() .isLength({ min: 3, max: 20 }) .withMessage('Username must be 3-20 characters') .isAlphanumeric() .withMessage('Username must be alphanumeric'), body('email') .trim() .isEmail() .withMessage('Invalid email') .normalizeEmail(), body('password') .isLength({ min: 8 }) .withMessage('Password must be at least 8 characters') ]; }; const updateUserValidationRules = () => { return [ param('id') .isInt() .withMessage('Invalid user ID'), body('username') .optional() .trim() .isLength({ min: 3, max: 20 }) .isAlphanumeric(), body('email') .optional() .trim() .isEmail() .normalizeEmail() ]; }; // Middleware to check validation results const validate = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ success: false, errors: errors.array() }); } next(); }; module.exports = { userValidationRules, updateUserValidationRules, validate }; // routes/users.js const express = require('express'); const router = express.Router(); const { userValidationRules, updateUserValidationRules, validate } = require('../validators/userValidator'); router.post('/', userValidationRules(), validate, createUser); router.put('/:id', updateUserValidationRules(), validate, updateUser); module.exports = router;

Sanitization Best Practices

// Always sanitize user input app.post('/api/posts', [ body('title') .trim() // Remove whitespace .escape() // Prevent XSS .notEmpty() .withMessage('Title is required'), body('content') .trim() .escape() // Prevent XSS .isLength({ min: 10 }) .withMessage('Content must be at least 10 characters'), body('tags') .optional() .customSanitizer(value => { // Clean and normalize tags array if (Array.isArray(value)) { return value .map(tag => tag.trim().toLowerCase()) .filter(tag => tag.length > 0) .slice(0, 5); // Limit to 5 tags } return []; }) ], validate, createPost );
Security Warning: Always escape HTML content unless you explicitly need to allow HTML and have implemented proper HTML sanitization using libraries like DOMPurify or sanitize-html.

File Upload Validation

Validating file uploads requires special handling:

const multer = require('multer'); const path = require('path'); // Configure multer const storage = multer.diskStorage({ destination: './uploads/', filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); } }); const fileFilter = (req, file, cb) => { // Accept images only const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; if (allowedTypes.includes(file.mimetype)) { cb(null, true); } else { cb(new Error('Invalid file type. Only JPEG, PNG, and GIF allowed'), false); } }; const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 // 5MB max } }); // Upload endpoint with validation app.post('/api/upload', upload.single('avatar'), [ body('description') .optional() .trim() .isLength({ max: 200 }) .withMessage('Description must be max 200 characters') ], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ errors: errors.array() }); } if (!req.file) { return res.status(400).json({ message: 'No file uploaded' }); } res.json({ success: true, file: req.file, description: req.body.description }); } );
Exercise: Create a product API with comprehensive validation:
  • POST /api/products - Create product with validation for: name (3-100 chars), description (10-500 chars), price (positive float), category (enum: Electronics, Clothing, Food, Books), stock (non-negative integer), images (array of URLs), metadata (nested object)
  • Implement custom validator to check if category exists
  • Sanitize all string inputs (trim, escape)
  • Create reusable validation middleware
  • Return user-friendly error messages
  • Test with valid and invalid data

Validation Error Formatting

Format validation errors for better user experience:

const formatValidationErrors = (errors) => { return errors.array().reduce((acc, error) => { // Group errors by field if (!acc[error.path]) { acc[error.path] = []; } acc[error.path].push(error.msg); return acc; }, {}); }; app.post('/api/users', userValidationRules(), (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ success: false, message: 'Validation failed', errors: formatValidationErrors(errors) }); } // Proceed... }); // Error response format: // { // "success": false, // "message": "Validation failed", // "errors": { // "username": ["Username must be 3-20 characters", "Username must be alphanumeric"], // "email": ["Invalid email address"], // "password": ["Password must be at least 8 characters"] // } // }

In the next lesson, we'll learn how to implement comprehensive error handling in Express applications to gracefully handle validation errors, server errors, and custom error scenarios.