or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-patterns.mdcustom-extensions.mdindex.mdresults-data.mdschema-validation.mdvalidation-chains.mdvalidators-sanitizers.md
tile.json

custom-extensions.mddocs/

Custom Validation and Sanitization

Extend express-validator with custom validation logic and sanitization functions using the ExpressValidator class and custom methods.

Capabilities

ExpressValidator Class

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);

Inline Custom Validation

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);

Inline Custom Sanitization

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);

Advanced Custom Validator Patterns

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);

Reusable Custom Validation Library

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);

Meta Object Properties

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;
});