🎯

form-validation

🎯Skill

from bbeierle12/skill-mcp-claude

VibeIndex|
What it does

Validates form inputs using Zod schemas, providing type-safe, real-time validation with optimal UX timing and clear error messages.

πŸ“¦

Part of

bbeierle12/skill-mcp-claude(74 items)

form-validation

Installation

pip installInstall dependencies
pip install -r requirements.txt
PythonRun Python server
python skills_manager_api.py
PythonRun Python server
python server.py
pip installInstall Python package
pip install pyinstaller
PythonRun Python server
python -m PyInstaller --onefile --name "SkillsManager" --console ^
Server ConfigurationMCP server configuration block
{ "name": "skill-name", "description": "One-line description", "tags": ["t...
πŸ“– Extracted from docs: bbeierle12/skill-mcp-claude
11Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Schema-first validation with Zod, timing patterns (reward early, punish late), async validation, and error message design. Use when implementing form validation for any framework. The foundation skill that all framework-specific skills depend on.

Overview

# Form Validation

Schema-first validation using Zod as the single source of truth for both runtime validation and TypeScript types.

Quick Start

```typescript

import { z } from 'zod';

// 1. Define schema (validation + types in one place)

const schema = z.object({

email: z.string().min(1, 'Required').email('Invalid email'),

age: z.number().positive().optional()

});

// 2. Infer TypeScript types (never manually define)

type FormData = z.infer;

// 3. Use with form library

import { zodResolver } from '@hookform/resolvers/zod';

const { register } = useForm({

resolver: zodResolver(schema)

});

```

Core Principle: Reward Early, Punish Late

This is the optimal validation timing pattern backed by UX research:

| Event | Show Valid (βœ“) | Show Invalid (βœ—) | Why |

|-------|----------------|------------------|-----|

| On input | βœ… Immediately | ❌ Never | Don't yell while typing |

| On blur | βœ… Immediately | βœ… Yes | User finished, show errors |

| During correction | βœ… Immediately | βœ… Real-time | Let them fix quickly |

Implementation

```typescript

// React Hook Form

useForm({

mode: 'onBlur', // First validation on blur (punish late)

reValidateMode: 'onChange' // Re-validate on change (real-time correction)

});

// TanStack Form

useForm({

validators: {

onBlur: schema, // Validate on blur

onChange: schema // Re-validate on change (after touched)

}

});

```

Zod Schema Patterns

Basic Types

```typescript

import { z } from 'zod';

// Strings

z.string() // Any string

z.string().min(1, 'Required') // Non-empty (better than .nonempty())

z.string().email('Invalid email')

z.string().url('Invalid URL')

z.string().uuid('Invalid ID')

z.string().regex(/^\d{5}$/, 'Invalid ZIP')

// Numbers

z.number() // Any number

z.number().positive('Must be positive')

z.number().int('Must be whole number')

z.number().min(0).max(100)

// Booleans

z.boolean()

z.literal(true) // Must be exactly true

// Enums

z.enum(['admin', 'user', 'guest'])

// Arrays

z.array(z.string())

z.array(z.string()).min(1, 'Select at least one')

// Objects

z.object({

name: z.string(),

email: z.string().email()

})

```

Common Form Schemas

```typescript

// schemas/auth.ts

export const loginSchema = z.object({

email: z

.string()

.min(1, 'Please enter your email')

.email('Please enter a valid email'),

password: z

.string()

.min(1, 'Please enter your password'),

rememberMe: z.boolean().optional().default(false)

});

export const registrationSchema = z.object({

email: z

.string()

.min(1, 'Email is required')

.email('Please enter a valid email'),

password: z

.string()

.min(1, 'Password is required')

.min(8, 'Password must be at least 8 characters')

.regex(/[A-Z]/, 'Include at least one uppercase letter')

.regex(/[a-z]/, 'Include at least one lowercase letter')

.regex(/[0-9]/, 'Include at least one number'),

confirmPassword: z

.string()

.min(1, 'Please confirm your password')

}).refine(data => data.password === data.confirmPassword, {

message: 'Passwords do not match',

path: ['confirmPassword']

});

export const forgotPasswordSchema = z.object({

email: z

.string()

.min(1, 'Email is required')

.email('Please enter a valid email')

});

export const resetPasswordSchema = z.object({

password: z

.string()

.min(8, 'Password must be at least 8 characters'),

confirmPassword: z.string()

}).refine(data => data.password === data.confirmPassword, {

message: 'Passwords do not match',

path: ['confirmPassword']

});

```

```typescript

// schemas/profile.ts

export const profileSchema = z.object({

firstName: z.string().min(1, 'First name is required'),

lastName: z.string().min(1, 'Last name is required'),

email: z.string().email('Invalid email'),

phone: z

.string()

.regex(/^\+?[\d\s-()]+$/, 'Invalid phone number')

.optional()

.or(z.literal('')),

bio: z

.string()

.max(500, 'Bio must be 500 characters or less')

.optional()

});

export const addressSchema = z.object({

street: z.string().min(1, 'Street address is required'),

city: z.string().min(1, 'City is required'),

state: z.string().min(1, 'State is required'),

zip: z.string().regex(/^\d{5}(-\d{4})?$/, 'Invalid ZIP code'),

country: z.string().min(1, 'Country is required').default('US')

});

```

```typescript

// schemas/payment.ts

export const paymentSchema = z.object({

cardName: z.string().min(1, 'Name on card is required'),

cardNumber: z

.string()

.regex(/^\d{13,19}$/, 'Invalid card number')

.refine(val => luhnCheck(val), 'Invalid card number'),

expMonth: z

.string()

.regex(/^(0[1-9]|1[0-2])$/, 'Invalid month'),

expYear: z

.string()

.regex(/^\d{2}$/, 'Invalid year')

.refine(val => {

const year = parseInt(val, 10) + 2000;

return year >= new Date().getFullYear();

}, 'Card has expired'),

cvc: z.string().regex(/^\d{3,4}$/, 'Invalid CVC')

});

// Luhn algorithm for card validation

function luhnCheck(cardNumber: string): boolean {

let sum = 0;

let isEven = false;

for (let i = cardNumber.length - 1; i >= 0; i--) {

let digit = parseInt(cardNumber[i], 10);

if (isEven) {

digit *= 2;

if (digit > 9) digit -= 9;

}

sum += digit;

isEven = !isEven;

}

return sum % 10 === 0;

}

```

Advanced Patterns

#### Conditional Validation

```typescript

const orderSchema = z.object({

deliveryMethod: z.enum(['shipping', 'pickup']),

address: z.object({

street: z.string(),

city: z.string(),

zip: z.string()

}).optional()

}).refine(

data => {

if (data.deliveryMethod === 'shipping') {

return data.address?.street && data.address?.city && data.address?.zip;

}

return true;

},

{

message: 'Address is required for shipping',

path: ['address']

}

);

```

#### Cross-Field Validation

```typescript

const dateRangeSchema = z.object({

startDate: z.date(),

endDate: z.date()

}).refine(

data => data.endDate >= data.startDate,

{

message: 'End date must be after start date',

path: ['endDate']

}

);

```

#### Schema Composition

```typescript

// Base schemas

const nameSchema = z.object({

firstName: z.string().min(1),

lastName: z.string().min(1)

});

const contactSchema = z.object({

email: z.string().email(),

phone: z.string().optional()

});

// Composed schema

const userSchema = nameSchema.merge(contactSchema).extend({

role: z.enum(['admin', 'user'])

});

```

Async Validation

For server-side checks (username availability, email uniqueness):

```typescript

// With Zod refine

const usernameSchema = z

.string()

.min(3, 'Username must be at least 3 characters')

.refine(

async (username) => {

const response = await fetch(/api/check-username?u=${encodeURIComponent(username)});

const { available } = await response.json();

return available;

},

{ message: 'This username is already taken' }

);

// With TanStack Form (built-in debouncing)

const form = useForm({

defaultValues: { username: '' },

validators: {

onChangeAsyncDebounceMs: 500,

onChangeAsync: async ({ value }) => {

const response = await fetch(/api/check-username?u=${value.username});

const { available } = await response.json();

if (!available) {

return { fields: { username: 'Username is taken' } };

}

return undefined;

}

}

});

```

Debounced Validation Helper

```typescript

// utils/debounced-validator.ts

export function createDebouncedValidator(

validator: (value: T) => Promise,

delay: number = 500

) {

let timeoutId: ReturnType;

let latestValue: T;

return (value: T): Promise => {

latestValue = value;

return new Promise((resolve) => {

clearTimeout(timeoutId);

timeoutId = setTimeout(async () => {

// Only validate if this is still the latest value

if (value === latestValue) {

const error = await validator(value);

resolve(error);

} else {

resolve(undefined);

}

}, delay);

});

};

}

// Usage

const checkUsername = createDebouncedValidator(async (username: string) => {

const response = await fetch(/api/check-username?u=${username});

const { available } = await response.json();

return available ? undefined : 'Username is taken';

}, 500);

```

Error Messages

Principles

  1. Specific: Tell users exactly what's wrong
  2. Actionable: Tell users how to fix it
  3. Contextual: Reference the field name
  4. Friendly: Don't blame the user

Examples

```typescript

// ❌ BAD: Generic, unhelpful

const badSchema = z.object({

email: z.string().email(), // "Invalid"

password: z.string().min(8), // "Too short"

phone: z.string().regex(/^\d+$/) // "Invalid"

});

// βœ… GOOD: Specific, actionable

const goodSchema = z.object({

email: z

.string()

.min(1, 'Please enter your email address')

.email('Please enter a valid email (e.g., name@example.com)'),

password: z

.string()

.min(1, 'Please create a password')

.min(8, 'Password must be at least 8 characters'),

phone: z

.string()

.regex(/^\d{10}$/, 'Please enter a 10-digit phone number')

});

```

Message Templates

```typescript

// utils/validation-messages.ts

export const messages = {

required: (field: string) => Please enter your ${field},

email: 'Please enter a valid email address',

minLength: (field: string, min: number) =>

${field} must be at least ${min} characters,

maxLength: (field: string, max: number) =>

${field} must be ${max} characters or less,

pattern: (field: string, example: string) =>

Please enter a valid ${field} (e.g., ${example}),

match: (field: string) => ${field} fields must match,

unique: (field: string) => This ${field} is already in use,

future: (field: string) => ${field} must be a future date,

past: (field: string) => ${field} must be a past date

};

// Usage

const schema = z.object({

email: z

.string()

.min(1, messages.required('email'))

.email(messages.email),

password: z

.string()

.min(1, messages.required('password'))

.min(8, messages.minLength('Password', 8))

});

```

Validation Timing Utility

```typescript

// utils/validation-timing.ts

export type ValidationMode = 'onBlur' | 'onChange' | 'onSubmit' | 'all';

export interface ValidationTimingConfig {

/* When to first show errors /

showErrorsOn: ValidationMode;

/* When to re-validate after first error /

revalidateOn: ValidationMode;

/* Debounce delay for onChange (ms) /

debounceMs?: number;

}

export const TIMING_PRESETS = {

/* Default: Reward early, punish late /

standard: {

showErrorsOn: 'onBlur',

revalidateOn: 'onChange'

} as ValidationTimingConfig,

/* For password strength, character counts /

realtime: {

showErrorsOn: 'onChange',

revalidateOn: 'onChange'

} as ValidationTimingConfig,

/* For simple, short forms /

submitOnly: {

showErrorsOn: 'onSubmit',

revalidateOn: 'onSubmit'

} as ValidationTimingConfig,

/* For expensive async validation /

debounced: {

showErrorsOn: 'onBlur',

revalidateOn: 'onChange',

debounceMs: 500

} as ValidationTimingConfig

} as const;

// React Hook Form mapping

export function toRHFConfig(timing: ValidationTimingConfig) {

return {

mode: timing.showErrorsOn === 'all' ? 'all' : timing.showErrorsOn,

reValidateMode: timing.revalidateOn === 'all' ? 'onChange' : timing.revalidateOn

};

}

```

File Structure

```

form-validation/

β”œβ”€β”€ SKILL.md

β”œβ”€β”€ references/

β”‚ β”œβ”€β”€ zod-patterns.md # Deep-dive Zod patterns

β”‚ β”œβ”€β”€ timing-research.md # UX research on validation timing

β”‚ └── error-message-guide.md # Writing good error messages

└── scripts/

β”œβ”€β”€ schemas/

β”‚ β”œβ”€β”€ auth.ts # Login, registration, password reset

β”‚ β”œβ”€β”€ profile.ts # User profile, addresses

β”‚ β”œβ”€β”€ payment.ts # Credit cards, billing

β”‚ └── common.ts # Reusable field schemas

β”œβ”€β”€ validation-timing.ts # Timing utilities

β”œβ”€β”€ async-validator.ts # Debounced async validation

└── messages.ts # Error message templates

```

Framework Integration

| Framework | Adapter | Import |

|-----------|---------|--------|

| React Hook Form | @hookform/resolvers/zod | zodResolver(schema) |

| TanStack Form | @tanstack/zod-form-adapter | zodValidator() |

| VeeValidate | @vee-validate/zod | toTypedSchema(schema) |

| Vanilla | Direct | schema.safeParse(data) |

Reference

  • references/zod-patterns.md β€” Complete Zod API patterns
  • references/timing-research.md β€” UX research backing timing decisions
  • references/error-message-guide.md β€” Writing effective error messages