Table of Contents

Auto Server Validation

CommandForm can automatically trigger server-side validation when all client-side validations pass. This provides real-time feedback for server-side validation rules (like checking if a username is already taken) without requiring an explicit form submission.

Overview

Auto server validation is designed for scenarios where you need to validate data against server-side business rules while users are filling out a form. Instead of waiting for form submission to discover that a username is taken or an email is already registered, users get immediate feedback as they type.

Key features:

  • Automatically calls command.validate() when all client validations pass
  • Configurable throttling to prevent excessive server calls
  • Seamlessly integrates with CommandForm's existing validation system
  • Works with any backend validation endpoint

Basic Usage

Enable auto server validation by setting the autoServerValidate prop:

<CommandForm 
    command={RegisterUser} 
    validateOn="change"
    autoServerValidate={true}
>
    <InputTextField<RegisterUser> value={c => c.username} title="Username" required />
    <InputTextField<RegisterUser> value={c => c.email} type="email" title="Email" required />
</CommandForm>

When enabled, the form will automatically call the command's validate() method (which makes an HTTP request to /api/{command-route}/validate) whenever:

  1. All client-side validations have passed
  2. The command data changes
  3. Any pending throttle timer has expired (if configured)

Validation Flow

User types → Client validation runs → All fields valid? 
    ↓ Yes
Throttle timer starts (if configured) → Timer expires → HTTP POST /validate
    ↓
Server validates → Returns errors → Form displays errors

If the user continues typing before the throttle timer expires, the timer resets and starts over.

Throttling

To avoid excessive server calls during rapid typing, use the autoServerValidateThrottle prop to delay validation:

<CommandForm 
    command={RegisterUser} 
    validateOn="change"
    autoServerValidate={true}
    autoServerValidateThrottle={500}  // Wait 500ms after typing stops
>
    <InputTextField<RegisterUser> value={c => c.username} title="Username" required />
    <InputTextField<RegisterUser> value={c => c.email} type="email" title="Email" required />
</CommandForm>

Throttle Values

The throttle value is in milliseconds:

  • 500 (default): Good balance between responsiveness and server load - recommended for most forms
  • 300: More responsive but more server calls
  • 1000: Less responsive but fewer server calls
  • 0: No throttle, validation occurs immediately (not recommended for validateOn="change")

How Throttling Works

  1. User makes a change that passes all client validations
  2. Timer starts counting down from the throttle value
  3. If user makes another change before timer expires:
    • Previous timer is cancelled
    • New timer starts from the beginning
  4. When timer expires without interruption:
    • Server validation is triggered
  5. If server returns errors:
    • Errors are displayed in the form
    • Timer resets for next validation

Example: With a 500ms throttle, typing "john@example.com" character by character:

  • Without throttle: 17 server calls (one per character)
  • With throttle: 1 server call (after user stops typing for 500ms)

Backend Implementation

For auto server validation to work, your backend must implement a validation endpoint that returns validation errors without executing the command.

C# Example

public record RegisterUser(string Username, string Email, string Password);

public class RegisterUserValidator : CommandValidator<RegisterUser>
{
    private readonly IUserRepository _users;
    
    public RegisterUserValidator(IUserRepository users)
    {
        _users = users;
        
        RuleFor(x => x.Username)
            .NotEmpty()
            .MinLength(3)
            .MaxLength(20)
            .MustAsync(async (username, ct) => !await _users.UsernameExists(username, ct))
            .WithMessage("This username is already taken");
            
        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .MustAsync(async (email, ct) => !await _users.EmailExists(email, ct))
            .WithMessage("This email address is already registered");
            
        RuleFor(x => x.Password)
            .NotEmpty()
            .MinLength(8)
            .WithMessage("Password must be at least 8 characters");
    }
}

The framework automatically exposes the validation endpoint at /api/register-user/validate. When called, it:

  1. Deserializes the command from the request
  2. Runs all validation rules
  3. Returns errors without executing the command handler

Validation Response

The validation endpoint returns a CommandResult with validation errors:

{
  "isSuccess": false,
  "validationErrors": {
    "username": ["This username is already taken"],
    "email": ["This email address is already registered"]
  }
}

Frontend Integration

The frontend automatically handles the validation response and displays errors:

<CommandForm 
    command={RegisterUser} 
    validateOn="change"
    autoServerValidate={true}
    autoServerValidateThrottle={500}
>
    <InputTextField<RegisterUser> 
        value={c => c.username} 
        title="Username" 
        placeholder="Choose a username"
        required 
        minLength={3}
        maxLength={20}
    />
    {/* Error appears here: "This username is already taken" */}
    
    <InputTextField<RegisterUser> 
        value={c => c.email} 
        type="email" 
        title="Email" 
        placeholder="Enter your email"
        required 
    />
    {/* Error appears here: "This email address is already registered" */}
    
    <InputTextField<RegisterUser> 
        value={c => c.password} 
        type="password" 
        title="Password" 
        placeholder="Min 8 characters"
        required 
        minLength={8}
    />
    {/* Error appears here: "Password must be at least 8 characters" */}
</CommandForm>

When to Use Auto Server Validation

Good Use Cases

Auto server validation is ideal for:

  • Uniqueness checks: Username, email, phone number availability
  • External validation: Coupon codes, invite codes, API keys
  • Domain validation: Domain name availability for SaaS products
  • Real-time quotas: Check if user has reached limits
  • Complex business rules: Rules that require database lookups or external services

When to Avoid

Consider manual validation (validate on submit) instead when:

  • Slow endpoints: Validation takes more than 1 second
  • High server load: Validation is expensive (complex queries, external APIs)
  • Many interdependent fields: Better to validate entire form at once
  • Sequential validation: One field's validation depends on another's result
  • Privacy concerns: You don't want to reveal information before submission

Performance Considerations

1. Use Appropriate Throttle Values

// Good: Balanced throttle for username checking
<CommandForm autoServerValidate autoServerValidateThrottle={500}>

// Avoid: No throttle with validateOn="change"
<CommandForm autoServerValidate autoServerValidateThrottle={0} validateOn="change">

// Consider: Longer throttle for expensive validation
<CommandForm autoServerValidate autoServerValidateThrottle={1000}>

2. Optimize Backend Validators

Keep validation queries fast:

// Good: Indexed lookup
public async Task<bool> UsernameExists(string username, CancellationToken ct)
{
    return await _db.Users
        .Where(u => u.Username == username)  // Username column should be indexed
        .AnyAsync(ct);
}

// Avoid: Full table scan or complex joins
public async Task<bool> UsernameExists(string username, CancellationToken ct)
{
    var allUsers = await _db.Users.Include(u => u.Profile).ToListAsync(ct);
    return allUsers.Any(u => u.Username == username);
}

3. Implement Rate Limiting

Protect your validation endpoints from abuse:

[HttpPost("validate")]
[RateLimitAttribute(MaxRequests = 10, TimeWindowSeconds = 60)]
public async Task<CommandResult> Validate([FromBody] RegisterUser command)
{
    // Validation logic
}

4. Cache Validation Results

For expensive validations, consider caching:

public class RegisterUserValidator : CommandValidator<RegisterUser>
{
    private readonly IUserRepository _users;
    private readonly IMemoryCache _cache;
    
    public RegisterUserValidator(IUserRepository users, IMemoryCache cache)
    {
        _users = users;
        _cache = cache;
        
        RuleFor(x => x.Username)
            .MustAsync(async (username, ct) => 
            {
                var cacheKey = $"username_exists_{username}";
                if (_cache.TryGetValue(cacheKey, out bool exists))
                {
                    return !exists;
                }
                
                exists = await _users.UsernameExists(username, ct);
                _cache.Set(cacheKey, exists, TimeSpan.FromMinutes(5));
                return !exists;
            })
            .WithMessage("This username is already taken");
    }
}

5. Combine with Client Validation

Always validate client-side first to reduce unnecessary server calls:

<CommandForm 
    autoServerValidate 
    autoServerValidateThrottle={500}
    validateOn="change"  // Client validation on every change
>
    <InputTextField 
        value={c => c.username}
        required              // Client-side required check
        minLength={3}        // Client-side length check
        maxLength={20}       // Client-side length check
        pattern="[a-zA-Z0-9_]+"  // Client-side pattern check
    />
    {/* Server validation only runs after client validation passes */}
</CommandForm>

Complete Example

Here's a full registration form with auto server validation:

import { CommandForm, InputTextField } from '@cratis/applications-react/commands';
import { RegisterUser } from './commands';

function RegistrationForm() {
    return (
        <div className="registration-form">
            <h2>Create Your Account</h2>
            
            <CommandForm 
                command={RegisterUser} 
                validateOn="change"
                autoServerValidate={true}
                autoServerValidateThrottle={500}
                onSuccess={() => {
                    console.log('Registration successful!');
                    // Redirect to dashboard
                }}
            >
                <InputTextField<RegisterUser> 
                    value={c => c.username} 
                    title="Username" 
                    placeholder="Choose a unique username"
                    required 
                    minLength={3}
                    maxLength={20}
                    pattern="[a-zA-Z0-9_]+"
                />
                <small>3-20 characters, letters, numbers, and underscores only</small>
                
                <InputTextField<RegisterUser> 
                    value={c => c.email} 
                    type="email" 
                    title="Email Address" 
                    placeholder="your.email@example.com"
                    required 
                />
                <small>We'll send a verification email</small>
                
                <InputTextField<RegisterUser> 
                    value={c => c.password} 
                    type="password" 
                    title="Password" 
                    placeholder="Min 8 characters"
                    required 
                    minLength={8}
                />
                <small>At least 8 characters</small>
                
                <button type="submit">Create Account</button>
            </CommandForm>
            
            <p className="help-text">
                Your username and email will be checked for availability as you type.
                Please wait for validation to complete before submitting.
            </p>
        </div>
    );
}

What Happens

  1. User types username: "john"

    • Client validation: ✓ Passes (3 chars, valid pattern)
    • Throttle: 500ms timer starts
  2. User continues typing: "john_"

    • Client validation: ✓ Passes (5 chars, valid pattern)
    • Throttle: Previous timer cancelled, new 500ms timer starts
  3. User continues typing: "john_smith"

    • Client validation: ✓ Passes (10 chars, valid pattern)
    • Throttle: Previous timer cancelled, new 500ms timer starts
  4. User stops typing for 500ms

    • Server validation: HTTP POST /api/register-user/validate
    • Response: { "validationErrors": { "username": ["This username is already taken"] } }
    • Form: Error displayed below username field
  5. User changes username to "john_smith_2024"

    • Client validation: ✓ Passes
    • Throttle: 500ms timer starts
    • User stops typing for 500ms
    • Server validation: HTTP POST /api/register-user/validate
    • Response: { "isSuccess": true }
    • Form: No errors, username is available

Integration with Other Validation Props

Auto server validation works alongside other validation props:

<CommandForm 
    validateOn="change"              // When to run client validation
    validateAllFieldsOnChange={false} // Validate only changed field
    validateOnInit={false}           // Don't validate empty form
    autoServerValidate={true}        // Enable server validation
    autoServerValidateThrottle={500} // Throttle server calls
>
    {/* fields */}
</CommandForm>

Interaction:

  • validateOn controls when client validation runs
  • autoServerValidate controls whether server validation runs automatically
  • Server validation only triggers when ALL client validations pass
  • autoServerValidateThrottle delays server validation, not client validation

Best Practices

  1. Set reasonable throttle values: 300-500ms for most forms, 1000ms+ for expensive validation
  2. Optimize backend validators: Use database indexes, caching, and efficient queries
  3. Always validate client-side first: Reduce server load by catching simple errors client-side
  4. Provide visual feedback: Show loading state during server validation (see Customization)
  5. Handle errors gracefully: Display clear, actionable error messages
  6. Implement rate limiting: Protect validation endpoints from abuse
  7. Consider user experience: Don't validate too aggressively - let users finish typing
  8. Test throttle behavior: Verify throttle works correctly with rapid typing
  9. Monitor server load: Track validation endpoint performance and adjust throttle if needed
  10. Document validation rules: Make it clear what server-side rules exist

See Also