Command Validation
Command validation enables pre-flight validation of commands without executing them. This provides early feedback to users in React applications before performing potentially expensive or state-changing operations.
Purpose
Section titled “Purpose”The validation mechanism allows you to check authorization and validation rules without executing the command handler. This is essential for:
- Early User Feedback: Show validation errors before the user submits a form
- UX Improvements: Enable/disable submit buttons based on validation state
- Authorization Checks: Verify user permissions without side effects
- Progressive Validation: Validate fields as users interact with forms
How It Works
Section titled “How It Works”When you validate a command, the request is sent to the backend validation endpoint where:
- All command filters run (authorization, validation)
- The command handler is not executed
- A
CommandResultis returned with validation and authorization status - No side effects occur on the system
For details on:
- The TypeScript command validation API, see Core Validation
- The backend validation pipeline, see Backend Command Validation
React Hook Usage
Section titled “React Hook Usage”React commands created with .use() include the validate() method:
import { CreateOrder } from './generated/commands';
function OrderForm() { const [command, setValues] = CreateOrder.use(); const [validationErrors, setValidationErrors] = useState<string[]>([]);
const handleFieldBlur = async () => { // Validate on field blur for early feedback const result = await command.validate();
if (!result.isValid) { setValidationErrors(result.validationResults.map(v => v.message)); } else { setValidationErrors([]); } };
const handleSubmit = async () => { // Execute the command const result = await command.execute();
if (result.isSuccess) { // Handle success } };
return ( <form onSubmit={handleSubmit}> <input value={command.orderNumber} onChange={e => command.orderNumber = e.target.value} onBlur={handleFieldBlur} /> {validationErrors.map(error => ( <div key={error} className="error">{error}</div> ))} <button type="submit">Create Order</button> </form> );}Progressive Validation
Section titled “Progressive Validation”You can validate commands reactively as properties change:
function ProductOrderForm() { const [command, setValues] = CreateOrder.use(); const [canSubmit, setCanSubmit] = useState(false);
useEffect(() => { // Validate whenever command properties change const validateCommand = async () => { const result = await command.validate(); setCanSubmit(result.isSuccess); };
validateCommand(); }, [command.hasChanges]);
return ( <form> <input value={command.productId} onChange={e => command.productId = e.target.value} /> <input value={command.quantity} onChange={e => command.quantity = parseInt(e.target.value)} /> <button type="submit" disabled={!canSubmit} onClick={() => command.execute()} > Create Order </button> </form> );}Debounced Validation
Section titled “Debounced Validation”To avoid excessive server calls during typing, debounce your validation:
import { useMemo, useEffect } from 'react';import { debounce } from 'lodash';
function OrderForm() { const [command] = CreateOrder.use(); const [errors, setErrors] = useState<string[]>([]);
const debouncedValidate = useMemo( () => debounce(async () => { const result = await command.validate();
if (!result.isSuccess) { setErrors(result.validationResults.map(v => v.message)); } else { setErrors([]); } }, 500), [command] );
useEffect(() => { if (command.hasChanges) { debouncedValidate(); } }, [command.orderNumber, command.quantity, debouncedValidate]);
return ( <form> <input value={command.orderNumber} onChange={e => command.orderNumber = e.target.value} /> {errors.map(error => ( <div key={error} className="error">{error}</div> ))} <button onClick={() => command.execute()}> Create Order </button> </form> );}Validation on Blur
Section titled “Validation on Blur”A common pattern is to validate when a field loses focus:
function CustomerForm() { const [command] = CreateCustomer.use(); const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const validateField = async (fieldName: string) => { const result = await command.validate();
if (!result.isValid) { const fieldError = result.validationResults.find( v => v.members.includes(fieldName) );
if (fieldError) { setFieldErrors(prev => ({ ...prev, [fieldName]: fieldError.message })); } } else { setFieldErrors(prev => { const { [fieldName]: _, ...rest } = prev; return rest; }); } };
return ( <form> <div> <input value={command.email} onChange={e => command.email = e.target.value} onBlur={() => validateField('email')} /> {fieldErrors.email && ( <span className="error">{fieldErrors.email}</span> )} </div> <div> <input value={command.phone} onChange={e => command.phone = e.target.value} onBlur={() => validateField('phone')} /> {fieldErrors.phone && ( <span className="error">{fieldErrors.phone}</span> )} </div> <button onClick={() => command.execute()}> Create Customer </button> </form> );}CommandResult Structure
Section titled “CommandResult Structure”Both execute() and validate() return the same CommandResult structure:
interface CommandResult<TResponse> { correlationId: string; isSuccess: boolean; // Overall success (authorized + valid + no exceptions) isAuthorized: boolean; // Authorization status isValid: boolean; // Validation status hasExceptions: boolean; // Whether exceptions occurred validationResults: ValidationResult[]; exceptionMessages: string[]; exceptionStackTrace: string; response?: TResponse; // Only populated on execute()}Note: The response property will be null or undefined when using validate() since the handler is not executed.
Best Practices
Section titled “Best Practices”When to Use Validate
Section titled “When to Use Validate”✅ Good Use Cases:
- Form validation as users type or blur fields
- Enabling/disabling submit buttons based on validation state
- Showing validation messages before submission
- Checking authorization before showing UI elements
❌ Avoid:
- Calling validate() immediately before execute() (execute already validates)
- Over-validating (don’t validate on every keystroke without debouncing)
- Using validate() as a substitute for client-side validation
Performance Considerations
Section titled “Performance Considerations”- Validation makes a server round-trip, so use judiciously
- Always debounce validation calls for real-time feedback
- Client-side validation is still important for immediate feedback
- Server validation ensures security and data integrity
Security Considerations
Section titled “Security Considerations”- Validation endpoints run the same authorization filters as execute endpoints
- Unauthorized users receive 401/403 responses from validation endpoints
- Validation does not expose sensitive data since handlers aren’t executed
- Validation results may reveal authorization policies (by design)
Troubleshooting
Section titled “Troubleshooting”Validation is slow
Section titled “Validation is slow”Cause: Complex validation logic or database queries in validators.
Solution:
- Debounce validation calls
- Optimize validator implementations on the backend
- Consider client-side validation for immediate feedback
Validation passes but execute fails
Section titled “Validation passes but execute fails”Cause: State may have changed between validate and execute calls, or the handler encountered an error.
Solution: This is expected behavior. Always check the result of execute() for the authoritative status.