CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-express-validator

Express middleware for comprehensive request validation and sanitization.

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

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

docs

advanced-patterns.md

custom-extensions.md

index.md

results-data.md

schema-validation.md

validation-chains.md

validators-sanitizers.md

tile.json