data-validation

Complete data validation patterns for JKKN COE Next.js application. This skill should be used when implementing form validation, import validation, or API-level validation. Automatically triggers when

SKILL.md

GitHub

Data Validation Skill

This skill provides comprehensive validation patterns for the JKKN COE application, covering client-side form validation, import data validation, and server-side API validation.

When to Use This Skill

Use this skill when:

  • Implementing form validation for CRUD operations
  • Validating imported Excel/CSV/JSON data
  • Creating API-level validation in route handlers
  • Standardizing error messages across the application
  • Handling foreign key validation

File Locations (Following project-structure)

lib/utils/
├── [entity]-validation.ts      # Entity-specific validation
├── validation-helpers.ts       # Shared validation utilities

Validation Function Pattern

1. Entity Validation Function

Create a dedicated validation file for each entity:

File: lib/utils/[entity]-validation.ts

export function validate[Entity]Data(
  data: Record<string, unknown>,
  rowIndex?: number
): string[] {
  const errors: string[] = []
  const prefix = rowIndex ? `Row ${rowIndex}: ` : ''

  // Required field validations
  if (!data.[entity]_code || String(data.[entity]_code).trim() === '') {
    errors.push(`${prefix}Entity Code is required`)
  } else if (String(data.[entity]_code).length > 50) {
    errors.push(`${prefix}Entity Code must be 50 characters or less`)
  }

  if (!data.[entity]_name || String(data.[entity]_name).trim() === '') {
    errors.push(`${prefix}Entity Name is required`)
  } else if (String(data.[entity]_name).length > 200) {
    errors.push(`${prefix}Entity Name must be 200 characters or less`)
  }

  // Format validations (see patterns below)
  // ...

  return errors
}

Validation Patterns

Required Field Validation

// Simple required check
if (!data.field_name || String(data.field_name).trim() === '') {
  errors.push('Field Name is required')
}

// With length constraint
if (!data.field_name || String(data.field_name).trim() === '') {
  errors.push('Field Name is required')
} else if (String(data.field_name).length > 100) {
  errors.push('Field Name must be 100 characters or less')
}

Email Validation

if (data.email && String(data.email).trim() !== '') {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  if (!emailRegex.test(String(data.email))) {
    errors.push('Email format is invalid')
  } else if (String(data.email).length > 100) {
    errors.push('Email must be 100 characters or less')
  }
}

Phone Validation

if (data.phone && String(data.phone).trim() !== '') {
  const phoneRegex = /^[\+]?[0-9\s\-\(\)]{10,15}$/
  if (!phoneRegex.test(String(data.phone))) {
    errors.push('Phone number format is invalid (10-15 digits)')
  }
}

URL/Website Validation

if (data.website && String(data.website).trim() !== '') {
  try {
    new URL(String(data.website))
  } catch {
    errors.push('Website URL format is invalid')
  }
  if (String(data.website).length > 255) {
    errors.push('Website URL must be 255 characters or less')
  }
}

PIN Code Validation (India)

if (data.pin_code && String(data.pin_code).trim() !== '') {
  const pinRegex = /^[0-9]{6}$/
  if (!pinRegex.test(String(data.pin_code))) {
    errors.push('PIN Code must be exactly 6 digits')
  }
}

Enum/Allowed Values Validation

if (data.entity_type && String(data.entity_type).trim() !== '') {
  const validTypes = ['type1', 'type2', 'type3']
  if (!validTypes.includes(String(data.entity_type))) {
    errors.push(`Entity Type must be one of: ${validTypes.join(', ')}`)
  }
}

Numeric Range Validation

if (data.credits !== undefined && data.credits !== '') {
  const credits = Number(data.credits)
  if (isNaN(credits) || credits < 0 || credits > 10) {
    errors.push('Credits must be between 0 and 10')
  }
}

Boolean/Status Validation

if (data.is_active !== undefined && data.is_active !== null) {
  if (typeof data.is_active !== 'boolean') {
    const statusValue = String(data.is_active).toLowerCase()
    if (!['true', 'false', 'active', 'inactive'].includes(statusValue)) {
      errors.push('Status must be true/false or Active/Inactive')
    }
  }
}

Conditional Validation

// Theory credit required when split credit is enabled
if (data.split_credit) {
  if (!data.theory_credit || Number(data.theory_credit) === 0) {
    errors.push('Theory credit is required when split credit is enabled')
  }
  if (!data.practical_credit || Number(data.practical_credit) === 0) {
    errors.push('Practical credit is required when split credit is enabled')
  }
}

Code Format Validation

// Alphanumeric with hyphens and underscores
if (data.course_code && !/^[A-Za-z0-9\-_]+$/.test(String(data.course_code))) {
  errors.push('Course code can only contain letters, numbers, hyphens, and underscores')
}

String Length Batch Validation

const stringFields = [
  { field: 'address_line1', maxLength: 255, name: 'Address Line 1' },
  { field: 'address_line2', maxLength: 255, name: 'Address Line 2' },
  { field: 'city', maxLength: 100, name: 'City' },
  { field: 'state', maxLength: 100, name: 'State' },
  { field: 'country', maxLength: 100, name: 'Country' },
]

stringFields.forEach(({ field, maxLength, name }) => {
  if (data[field] && String(data[field]).length > maxLength) {
    errors.push(`${name} must be ${maxLength} characters or less`)
  }
})

Form Validation Pattern

In-Page Validation Hook

const [errors, setErrors] = useState<Record<string, string>>({})

const validate = (): boolean => {
  const e: Record<string, string> = {}

  if (!formData.institution_code.trim()) {
    e.institution_code = 'Institution is required'
  }
  if (!formData.[entity]_code.trim()) {
    e.[entity]_code = 'Entity Code is required'
  }
  if (!formData.[entity]_name.trim()) {
    e.[entity]_name = 'Entity Name is required'
  }

  // Additional validations...

  setErrors(e)
  return Object.keys(e).length === 0
}

Form Field with Error Display

<div className="space-y-2">
  <Label htmlFor="[entity]_code">
    Entity Code <span className="text-red-500">*</span>
  </Label>
  <Input
    id="[entity]_code"
    value={formData.[entity]_code}
    onChange={(e) => setFormData({ ...formData, [entity]_code: e.target.value })}
    className={errors.[entity]_code ? 'border-red-500' : ''}
    placeholder="Enter entity code"
  />
  {errors.[entity]_code && (
    <p className="text-sm text-red-500">{errors.[entity]_code}</p>
  )}
</div>

Form Submission with Validation

const handleSubmit = async () => {
  if (!validate()) {
    toast({
      title: 'Validation Error',
      description: 'Please fix all validation errors before submitting.',
      variant: 'destructive'
    })
    return
  }

  try {
    setLoading(true)
    const response = await fetch('/api/[entity]', {
      method: editing ? 'PUT' : 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(formData)
    })

    if (!response.ok) {
      const errorData = await response.json()
      throw new Error(errorData.error || 'Failed to save')
    }

    // Success handling...
  } catch (error) {
    const errorMessage = error instanceof Error
      ? error.message
      : 'Failed to save. Please try again.'

    toast({
      title: 'Save Failed',
      description: errorMessage,
      variant: 'destructive'
    })
  } finally {
    setLoading(false)
  }
}

API-Level Validation

Server-Side Validation in Route Handler

// app/api/[entity]/route.ts
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
  try {
    const body = await request.json()

    // Required field validation
    if (!body.[entity]_code?.trim()) {
      return NextResponse.json(
        { error: 'Entity Code is required' },
        { status: 400 }
      )
    }

    if (!body.[entity]_name?.trim()) {
      return NextResponse.json(
        { error: 'Entity Name is required' },
        { status: 400 }
      )
    }

    // Foreign key validation
    const { data: institutionData, error: institutionError } = await supabase
      .from('institutions')
      .select('id')
      .eq('institution_code', body.institution_code)
      .single()

    if (institutionError || !institutionData) {
      return NextResponse.json(
        { error: `Institution "${body.institution_code}" not found` },
        { status: 400 }
      )
    }

    // Insert with validated data...

  } catch (error) {
    console.error('API error:', error)
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

PostgreSQL Error Code Handling

if (error) {
  console.error('Database error:', error)

  // Duplicate key constraint (23505)
  if (error.code === '23505') {
    return NextResponse.json(
      { error: 'Record already exists. Please use different values.' },
      { status: 400 }
    )
  }

  // Foreign key constraint (23503)
  if (error.code === '23503') {
    return NextResponse.json(
      { error: 'Invalid reference. Please select a valid option.' },
      { status: 400 }
    )
  }

  // Check constraint (23514)
  if (error.code === '23514') {
    return NextResponse.json(
      { error: 'Invalid value. Please check your input.' },
      { status: 400 }
    )
  }

  // Not-null constraint (23502)
  if (error.code === '23502') {
    return NextResponse.json(
      { error: 'Missing required field.' },
      { status: 400 }
    )
  }

  return NextResponse.json(
    { error: 'Failed to save record' },
    { status: 500 }
  )
}

Import Validation Pattern

// Validate each row during import
for (let i = 0; i < rows.length; i++) {
  const row = rows[i]
  const rowNumber = i + 2 // +2 for header row in Excel

  const validationErrors = validate[Entity]Data(row, rowNumber)

  if (validationErrors.length > 0) {
    uploadErrors.push({
      row: rowNumber,
      [entity]_code: row.[entity]_code || 'N/A',
      [entity]_name: row.[entity]_name || 'N/A',
      errors: validationErrors
    })
    continue
  }

  // Proceed with API save...
}

Complete Example: Institution Validation

See references/institution-validation-example.md for a complete implementation.

Testing Checklist

  • Required fields show errors when empty
  • Email format validation works
  • Phone format validation works
  • URL format validation works
  • Length constraints are enforced
  • Enum values are validated
  • Numeric ranges are validated
  • Conditional validation works
  • Error messages are user-friendly
  • API returns proper error codes
  • Import validation shows row numbers