Extend express-validator with custom validation logic and sanitization functions using the ExpressValidator class and custom methods.
Create custom validator instances with your own validators and sanitizers.
/**
* Main class for creating custom validator instances
* @param customValidators - Object containing custom validator functions
* @param customSanitizers - Object containing custom sanitizer functions
*/
class ExpressValidator {
constructor(
customValidators?: Record<string, CustomValidator>,
customSanitizers?: Record<string, CustomSanitizer>
);
// Standard validation chain builders
check(fields?: string | string[], message?: string): ValidationChain;
body(fields?: string | string[], message?: string): ValidationChain;
cookie(fields?: string | string[], message?: string): ValidationChain;
header(fields?: string | string[], message?: string): ValidationChain;
param(fields?: string | string[], message?: string): ValidationChain;
query(fields?: string | string[], message?: string): ValidationChain;
// Schema and advanced validation
checkSchema(schema: Schema, defaultLocations?: Location[]): ValidationChain[];
oneOf(chains: (ValidationChain | ValidationChain[])[], message?: string): ContextRunner;
checkExact(knownFields: string[], options?: ExactOptions): ContextRunner;
// Result and data extraction
validationResult(req: Request): Result;
matchedData(req: Request, options?: MatchedDataOptions): any;
}
type CustomValidator = (value: any, meta: Meta, ...args: any[]) => boolean | Promise<boolean>;
type CustomSanitizer = (value: any, meta: Meta, ...args: any[]) => any;Usage Examples:
import { ExpressValidator } from "express-validator";
// Define custom validators
const customValidators = {
isStrongPassword: (value: string) => {
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasNumber = /\d/.test(value);
const hasSpecial = /[!@#$%^&*]/.test(value);
return hasUpper && hasLower && hasNumber && hasSpecial && value.length >= 8;
},
isValidUsername: async (value: string) => {
if (!/^[a-zA-Z0-9_]+$/.test(value)) return false;
if (value.length < 3 || value.length > 20) return false;
// Check database for uniqueness
const exists = await User.findByUsername(value);
return !exists;
}
};
// Define custom sanitizers
const customSanitizers = {
normalizeUsername: (value: string) => {
return value.toLowerCase().trim().replace(/[^a-z0-9_]/g, '');
},
formatPhone: (value: string) => {
const digits = value.replace(/\D/g, '');
if (digits.length === 10) {
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
}
return value;
}
};
// Create custom validator instance
const validator = new ExpressValidator(customValidators, customSanitizers);
// Use custom validators in routes
app.post('/register', [
validator.body('username').isValidUsername(),
validator.body('password').isStrongPassword(),
validator.body('phone').normalizeUsername()
], registerHandler);Add custom validation logic directly to validation chains.
interface ValidationChain {
/**
* Add custom validation logic
* @param validator - Custom validation function
* @returns ValidationChain for continued chaining
*/
custom(validator: CustomValidator): ValidationChain;
}
type CustomValidator = (value: any, meta: Meta) => boolean | Promise<boolean>;Usage Examples:
import { body } from "express-validator";
// Simple custom validation
app.post('/user', [
body('age').custom(value => {
return value >= 18 && value <= 120;
}).withMessage('Age must be between 18 and 120'),
// Async custom validation
body('email').custom(async (value, { req }) => {
const user = await User.findByEmail(value);
if (user && user.id !== req.params.id) {
throw new Error('Email already in use');
}
return true;
}),
// Complex validation with metadata
body('confirmPassword').custom((value, { req, path }) => {
if (value !== req.body.password) {
throw new Error('Password confirmation does not match');
}
return true;
})
], userHandler);
// Cross-field validation
app.post('/event', [
body('startDate').isISO8601().toDate(),
body('endDate').isISO8601().toDate(),
body('endDate').custom((endDate, { req }) => {
const startDate = new Date(req.body.startDate);
const end = new Date(endDate);
if (end <= startDate) {
throw new Error('End date must be after start date');
}
return true;
})
], eventHandler);Add custom sanitization logic directly to validation chains.
interface ValidationChain {
/**
* Add custom sanitization logic
* @param sanitizer - Custom sanitization function
* @returns ValidationChain for continued chaining
*/
customSanitizer(sanitizer: CustomSanitizer): ValidationChain;
}
type CustomSanitizer = (value: any, meta: Meta) => any;Usage Examples:
import { body } from "express-validator";
app.post('/article', [
// Custom text processing
body('title').customSanitizer(value => {
return value.trim().replace(/\s+/g, ' ').toLowerCase();
}),
// Array processing
body('tags').customSanitizer(value => {
if (!Array.isArray(value)) return [];
return value
.map(tag => tag.trim().toLowerCase())
.filter(tag => tag.length > 0)
.slice(0, 10); // Limit to 10 tags
}),
// Complex data transformation
body('metadata').customSanitizer((value, { req }) => {
const defaults = { author: req.user.id, createdAt: new Date() };
return Object.assign(defaults, value || {});
})
], articleHandler);
// Phone number formatting
app.post('/contact', [
body('phone').customSanitizer(value => {
const digits = value.replace(/\D/g, '');
if (digits.length === 10) {
return `+1${digits}`;
}
return value;
}).isMobilePhone('any')
], contactHandler);Complex custom validation scenarios with error handling and async operations.
// Database validation with caching
const validateUniqueEmail = async (email, { req }) => {
// Skip validation if email hasn't changed
if (req.params.id) {
const currentUser = await User.findById(req.params.id);
if (currentUser && currentUser.email === email) {
return true;
}
}
const existingUser = await User.findByEmail(email);
if (existingUser) {
throw new Error('Email already registered');
}
return true;
};
// File validation
const validateImageUpload = (value, { req }) => {
if (!req.file) {
throw new Error('Image file required');
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(req.file.mimetype)) {
throw new Error('Only JPEG, PNG, and GIF images allowed');
}
const maxSize = 5 * 1024 * 1024; // 5MB
if (req.file.size > maxSize) {
throw new Error('Image must be smaller than 5MB');
}
return true;
};
// Business rule validation
const validateBusinessHours = (time, { req }) => {
const dayOfWeek = req.body.dayOfWeek;
const hour = parseInt(time.split(':')[0]);
// Different hours for different days
const businessHours = {
'monday': { start: 9, end: 17 },
'friday': { start: 9, end: 15 },
'saturday': { start: 10, end: 14 },
'sunday': null // Closed
};
const hours = businessHours[dayOfWeek];
if (!hours) {
throw new Error('Closed on this day');
}
if (hour < hours.start || hour >= hours.end) {
throw new Error(`Business hours: ${hours.start}:00-${hours.end}:00`);
}
return true;
};
// Usage
app.post('/appointment', [
body('email').custom(validateUniqueEmail),
body('profileImage').custom(validateImageUpload),
body('appointmentTime').custom(validateBusinessHours)
], appointmentHandler);Create a library of reusable custom validators and sanitizers.
// validators/custom.js
export const customValidators = {
// Password strength validation
isSecurePassword: (value: string) => {
const minLength = 12;
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasNumbers = /\d/.test(value);
const hasNonalphas = /\W/.test(value);
return value.length >= minLength &&
hasUpperCase && hasLowerCase &&
hasNumbers && hasNonalphas;
},
// Credit card validation
isCreditCardType: (value: string, cardType: string) => {
const patterns = {
visa: /^4[0-9]{12}(?:[0-9]{3})?$/,
mastercard: /^5[1-5][0-9]{14}$/,
amex: /^3[47][0-9]{13}$/,
discover: /^6(?:011|5[0-9]{2})[0-9]{12}$/
};
return patterns[cardType]?.test(value.replace(/\s/g, '')) || false;
},
// Social security number
isSSN: (value: string) => {
const ssnPattern = /^(?!666|000|9\d{2})\d{3}-?(?!00)\d{2}-?(?!0000)\d{4}$/;
return ssnPattern.test(value);
}
};
export const customSanitizers = {
// Normalize phone numbers
normalizePhone: (value: string, format: string = 'international') => {
const digits = value.replace(/\D/g, '');
if (format === 'international' && digits.length === 10) {
return `+1${digits}`;
} else if (format === 'us' && digits.length === 10) {
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
}
return value;
},
// Clean HTML but preserve basic formatting
sanitizeHtml: (value: string) => {
const allowedTags = ['b', 'i', 'em', 'strong', 'p', 'br'];
// Implementation would use a library like DOMPurify
return value; // Simplified for example
},
// Generate slug from title
generateSlug: (value: string) => {
return value
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
};
// Use in application
import { ExpressValidator } from "express-validator";
import { customValidators, customSanitizers } from "./validators/custom.js";
const validator = new ExpressValidator(customValidators, customSanitizers);
app.post('/secure-signup', [
validator.body('password').isSecurePassword()
.withMessage('Password must be at least 12 characters with mixed case, numbers, and symbols'),
validator.body('ssn').isSSN()
.withMessage('Invalid Social Security Number'),
validator.body('phone').normalizePhone('us'),
validator.body('title').generateSlug()
], signupHandler);Understanding the metadata object passed to custom validators and sanitizers.
interface Meta {
/** The express request from which the field was validated */
req: Request;
/** Which request object the field was picked from */
location: Location;
/** The full path of the field within the request object */
path: string;
/** Values from wildcards used when selecting fields */
pathValues: readonly (string | string[])[];
}Usage Examples:
// Using meta information in custom validators
body('items.*.price').custom((price, meta) => {
const { req, path, pathValues } = meta;
const itemIndex = pathValues[0]; // Index from wildcard
const item = req.body.items[itemIndex];
// Validate price based on item category
if (item.category === 'premium' && price < 100) {
throw new Error('Premium items must cost at least $100');
}
return true;
});
// Location-specific validation
check('token').custom((value, meta) => {
const { location } = meta;
if (location === 'headers') {
// Validate as Bearer token
return /^Bearer\s+[\w-]+$/.test(value);
} else if (location === 'query') {
// Validate as query parameter token
return /^[\w-]+$/.test(value);
}
return false;
});