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

results-data.mddocs/

Validation Results and Data Extraction

Extract validation results, handle errors, and retrieve validated data from requests after running validation middleware.

Capabilities

Validation Result Extraction

Extract validation results from requests to check for errors and handle validation outcomes.

/**
 * Extract validation result from request
 * @param req - Express request object
 * @returns Result instance containing validation outcomes
 */
function validationResult(req: Request): Result;

interface Result {
  /** Check if validation passed (no errors) */
  isEmpty(): boolean;
  
  /** Get validation errors as array */
  array(options?: { onlyFirstError?: boolean }): ValidationError[];
  
  /** Get validation errors mapped by field path */
  mapped(): Record<string, ValidationError>;
  
  /** Format errors using custom formatter */
  formatWith<T>(formatter: ErrorFormatter<T>): T;
  
  /** Throw validation errors if any exist */
  throw(): never;
}

type ErrorFormatter<T> = (error: ValidationError) => T;

Custom Result Factory

Create a validationResult-like function with default options for consistent error formatting across your application.

/**
 * Create a validationResult function with default options
 * @param options - Default options for all Result instances
 * @returns ResultFactory function that works like validationResult
 */
function withDefaults<T = any>(
  options?: Partial<ResultFactoryBuilderOptions<T>>
): ResultFactory<T>;

interface ResultFactoryBuilderOptions<T = any> {
  /** Default error formatter for all Result instances */
  formatter: ErrorFormatter<T>;
}

type ResultFactory<T> = (req: Request) => Result<T>;

Usage Examples:

import { validationResult } from "express-validator";

// Create custom validation result with default formatter
const customValidationResult = validationResult.withDefaults({
  formatter: (error) => ({
    field: error.path,
    message: error.msg,
    code: `ERR_${error.path.toUpperCase()}`
  })
});

// Use in middleware
app.post('/api/user', validationChains, (req, res) => {
  const errors = customValidationResult(req);
  
  if (!errors.isEmpty()) {
    // Errors are automatically formatted with default formatter
    return res.status(400).json({
      success: false,
      errors: errors.array() // Uses default formatter
    });
  }
});

// Create legacy format compatibility
const legacyValidationResult = validationResult.withDefaults({
  formatter: (error) => ({
    param: error.path,
    msg: error.msg,
    value: error.value
  })
});

// API versioning with different error formats
const v1ValidationResult = validationResult.withDefaults({
  formatter: (error) => ({ field: error.path, error: error.msg })
});

const v2ValidationResult = validationResult.withDefaults({
  formatter: (error) => ({ 
    path: error.path, 
    message: error.msg, 
    location: error.location,
    severity: 'error'
  })
});

Usage Examples:

import { body, validationResult } from "express-validator";

app.post('/user', [
  body('email').isEmail(),
  body('name').trim().isLength({ min: 2 }),
  body('age').isInt({ min: 18 })
], (req, res) => {
  const errors = validationResult(req);
  
  // Check if validation passed
  if (!errors.isEmpty()) {
    return res.status(400).json({
      success: false,
      errors: errors.array()
    });
  }
  
  // Proceed with valid data
  res.json({ success: true, message: 'User created' });
});

// Get only first error per field
app.post('/quick-validation', validationChains, (req, res) => {
  const errors = validationResult(req);
  
  if (!errors.isEmpty()) {
    return res.status(400).json({
      errors: errors.array({ onlyFirstError: true })
    });
  }
  
  // Handle success
});

Error Object Structure

Understanding the structure of validation errors returned by the Result object.

interface ValidationError {
  type: 'field' | 'alternative' | 'alternative_grouped' | 'unknown_fields';
  location: Location;
  path: string;
  value: any;
  msg: string;
}

interface AlternativeValidationError {
  type: 'alternative';
  msg: string;
  nestedErrors: ValidationError[];
}

interface UnknownFieldsError {
  type: 'unknown_fields';
  msg: string;
  fields: UnknownFieldInstance[];
}

Usage Examples:

// Handle different error types
app.post('/api/data', validationChains, (req, res) => {
  const errors = validationResult(req);
  
  if (!errors.isEmpty()) {
    const errorList = errors.array();
    
    // Separate different error types
    const fieldErrors = errorList.filter(err => err.type === 'field');
    const alternativeErrors = errorList.filter(err => err.type === 'alternative');
    
    return res.status(400).json({
      fieldErrors,
      alternativeErrors,
      count: errorList.length
    });
  }
});

Mapped Error Handling

Get errors organized by field path for easier frontend handling.

// Get errors mapped by field
app.post('/form', validationChains, (req, res) => {
  const errors = validationResult(req);
  
  if (!errors.isEmpty()) {
    const mappedErrors = errors.mapped();
    
    // mappedErrors structure:
    // {
    //   'email': { type: 'field', location: 'body', path: 'email', value: 'invalid', msg: 'Invalid email' },
    //   'name': { type: 'field', location: 'body', path: 'name', value: '', msg: 'Name is required' }
    // }
    
    return res.status(400).json({
      success: false,
      fieldErrors: mappedErrors
    });
  }
});

Custom Error Formatting

Format validation errors using custom formatters for consistent error responses.

// Custom error formatter
const customFormatter = (error: ValidationError) => ({
  field: error.path,
  message: error.msg,
  rejectedValue: error.value
});

app.post('/api/custom-errors', validationChains, (req, res) => {
  const errors = validationResult(req);
  
  if (!errors.isEmpty()) {
    const formattedErrors = errors.formatWith(customFormatter);
    
    return res.status(400).json({
      success: false,
      validation_errors: formattedErrors
    });
  }
});

// Legacy format compatibility
const legacyFormatter = (error: ValidationError) => ({
  param: error.path,
  msg: error.msg,
  value: error.value
});

Throwing Validation Errors

Automatically throw validation errors for centralized error handling.

// Middleware to auto-throw validation errors
const handleValidationErrors = (req, res, next) => {
  const errors = validationResult(req);
  
  try {
    errors.throw(); // Throws if errors exist
    next(); // Continue if no errors
  } catch (error) {
    // Handle thrown validation errors
    res.status(400).json({
      error: 'Validation failed',
      details: errors.array()
    });
  }
};

app.post('/auto-throw', validationChains, handleValidationErrors, (req, res) => {
  // This handler only runs if validation passed
  res.json({ success: true });
});

Matched Data Extraction

Extract only validated and sanitized data from requests.

/**
 * Extract validated data from request
 * @param req - Express request object
 * @param options - Options for data extraction
 * @returns Object containing only validated fields
 */
function matchedData(req: Request, options?: MatchedDataOptions): any;

interface MatchedDataOptions {
  /** Include only successfully validated data (default: true) */
  onlyValidData?: boolean;
  
  /** Include optional fields that weren't provided (default: false) */
  includeOptionals?: boolean;
  
  /** Specific locations to extract data from */
  locations?: Location[];
}

Usage Examples:

import { body, matchedData, validationResult } from "express-validator";

app.post('/user', [
  body('email').isEmail().normalizeEmail(),
  body('name').trim().isLength({ min: 2 }),
  body('age').isInt({ min: 18 }).toInt(),
  body('bio').optional().trim()
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  // Get only validated and sanitized data
  const userData = matchedData(req);
  
  // userData contains only: email, name, age, bio (if provided)
  // All data is sanitized (email normalized, name trimmed, age converted to int)
  
  console.log(userData);
  // { email: 'user@example.com', name: 'John Doe', age: 25 }
});

// Extract from specific locations only
app.get('/search', [
  query('q').trim().isLength({ min: 1 }),
  query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
  query('offset').optional().isInt({ min: 0 }).toInt()
], (req, res) => {
  const queryData = matchedData(req, { locations: ['query'] });
  
  // Only includes validated query parameters
  performSearch(queryData);
});

// Include optional fields
app.put('/profile', [
  body('name').trim().notEmpty(),
  body('bio').optional().trim(),
  body('website').optional().isURL()
], (req, res) => {
  const profileData = matchedData(req, { 
    includeOptionals: true 
  });
  
  // Includes optional fields even if not provided (as undefined)
  updateProfile(profileData);
});

Data Sanitization Benefits

Matched data automatically includes all sanitization transformations applied during validation.

app.post('/product', [
  body('name').trim().escape(), // Trimmed and HTML-escaped
  body('price').toFloat(), // Converted to number
  body('tags').toArray(), // Converted to array
  body('description').trim().stripLow() // Trimmed and low chars removed
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  const productData = matchedData(req);
  
  // All sanitization is already applied:
  // - name: trimmed and HTML-escaped
  // - price: converted to float
  // - tags: converted to array
  // - description: trimmed with low ASCII chars removed
  
  saveProduct(productData);
});

Combining Results and Data

Common patterns for handling validation results and extracting data.

// Utility function for validation handling
const validateAndExtract = (req, res, next) => {
  const errors = validationResult(req);
  
  if (!errors.isEmpty()) {
    return res.status(400).json({
      success: false,
      errors: errors.array(),
      message: 'Validation failed'
    });
  }
  
  // Add validated data to request for next middleware
  req.validatedData = matchedData(req);
  next();
};

app.post('/api/order', [
  body('items').isArray({ min: 1 }),
  body('shippingAddress.street').trim().notEmpty(),
  body('shippingAddress.city').trim().notEmpty(),
  body('paymentMethod').isIn(['card', 'paypal'])
], validateAndExtract, (req, res) => {
  // req.validatedData contains clean, validated data
  const { items, shippingAddress, paymentMethod } = req.validatedData;
  
  processOrder({ items, shippingAddress, paymentMethod });
  res.json({ success: true });
});

// Handle partial updates with matched data
app.patch('/user/:id', [
  param('id').isUUID(),
  body('name').optional().trim().isLength({ min: 2 }),
  body('email').optional().isEmail().normalizeEmail(),
  body('age').optional().isInt({ min: 18 }).toInt()
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  
  const { id } = matchedData(req, { locations: ['params'] });
  const updates = matchedData(req, { locations: ['body'] });
  
  updateUser(id, updates);
  res.json({ success: true });
});