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
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
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 Command Validation
- The backend validation pipeline, see Backend Command Validation
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
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
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
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
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
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
- 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
- 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
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
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.