Skip to main content
Version: 5.x

Using Vest with Svelte

Vest integrates naturally with Svelte's reactive stores and reactive statements, providing elegant validation for your forms.

Quick Start​

<script>
import { create, test, enforce } from 'vest';
import { writable } from 'svelte/store';

const suite = create((data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});

test('username', 'Username must be at least 3 characters', () => {
enforce(data.username).longerThanOrEquals(3);
});

test('email', 'Email is required', () => {
enforce(data.email).isNotBlank();
});

test('email', 'Please enter a valid email', () => {
enforce(data.email).isEmail();
});
});

let formData = {
username: '',
email: ''
};

let result = suite.get();

function validateField(fieldName) {
suite.afterEach((res) => {
result = res;
}).run(formData, fieldName);
}

function handleSubmit() {
suite.afterEach((res) => {
result = res;

if (!res.hasErrors()) {
console.log('Form is valid!', formData);
}
}).run(formData);
}
</script>

<form on:submit|preventDefault={handleSubmit}>
<div>
<input
bind:value={formData.username}
on:input={() => validateField('username')}
placeholder="Username"
/>
{#if result.hasErrors('username')}
<span class="error">{result.getErrors('username')[0]}</span>
{/if}
</div>

<div>
<input
bind:value={formData.email}
on:input={() => validateField('email')}
type="email"
placeholder="Email"
/>
{#if result.hasErrors('email')}
<span class="error">{result.getErrors('email')[0]}</span>
{/if}
</div>

<button type="submit" disabled={result.hasErrors()}>
Submit
</button>
</form>

<style>
.error {
color: red;
font-size: 0.875rem;
}
</style>

## Using Svelte Stores

Create a reactive store for your validation results:

```svelte
<script>
import { writable, derived } from 'svelte/store';
import { create, test, enforce } from 'vest';

const suite = create((data = {}) => {
test('email', 'Email is required', () => {
enforce(data.email).isNotBlank();
});

test('password', 'Password must be at least 8 characters', () => {
enforce(data.password).longerThanOrEquals(8);
});
});

const formData = writable({
email: '',
password: ''
});

const validationResult = writable(suite.get());

// Derived store for form validity
const isFormValid = derived(
validationResult,
$result => !$result.hasErrors()
);

function validateField(fieldName, value) {
formData.update(data => ({ ...data, [fieldName]: value }));

suite.afterEach((res) => {
validationResult.set(res);
}).run($formData, fieldName);
}

function handleSubmit() {
suite.afterEach((res) => {
validationResult.set(res);

if (!res.hasErrors()) {
console.log('Submitting:', $formData);
}
}).run($formData);
}
</script>

<form on:submit|preventDefault={handleSubmit}>
<input
value={$formData.email}
on:input={(e) => validateField('email', e.target.value)}
type="email"
/>
{#if $validationResult.hasErrors('email')}
<span>{$validationResult.getErrors('email')[0]}</span>
{/if}

<button type="submit" disabled={!$isFormValid}>
Submit
</button>
</form>

## Reactive Statements

Use Svelte's reactive statements for automatic validation:

```svelte
<script>
import { create, test, enforce } from 'vest';

const suite = create((data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
});

let username = '';
let result = suite.get();

// Automatically validate when username changes
$: if (username !== undefined) {
suite.afterEach((res) => {
result = res;
}).run({ username }, 'username');
}
</script>

<input bind:value={username} placeholder="Username" />
{#if result.hasErrors('username')}
<span>{result.getErrors('username')[0]}</span>
{/if}

Async Validation​

Handle async validations with Svelte's reactivity:

<script>
import { create, test, enforce } from 'vest';

const suite = create((data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});

test('username', 'Username is already taken', async () => {
await enforce(data.username).isNotBlank();

const response = await fetch(`/api/check-username?username=${data.username}`);
const { available } = await response.json();

enforce(available).isTruthy();
});
});

let username = '';
let result = suite.get();
let isChecking = false;

async function checkUsername() {
isChecking = true;

suite.afterEach((res) => {
result = res;
isChecking = false;
}).run({ username }, 'username');
}

// Debounced reactive check
let debounceTimer;
$: {
clearTimeout(debounceTimer);
if (username) {
debounceTimer = setTimeout(checkUsername, 500);
}
}
</script>

<div>
<input bind:value={username} placeholder="Choose a username" />
{#if isChecking}
<span>Checking availability...</span>
{:else if result.hasErrors('username')}
<span class="error">{result.getErrors('username')[0]}</span>
{:else if result.isValid('username')}
<span class="success">Username is available!</span>
{/if}
</div>

Felte Integration​

Vest integrates with Felte, a popular form library for Svelte:

npm install felte @felte/validator-vest
<script>
import { createForm } from 'felte';
import { validator } from '@felte/validator-vest';
import { create, test, enforce } from 'vest';

const suite = create((data = {}) => {
test('email', 'Email is required', () => {
enforce(data.email).isNotBlank();
});

test('email', 'Please enter a valid email', () => {
enforce(data.email).isEmail();
});

test('password', 'Password must be at least 8 characters', () => {
enforce(data.password).longerThanOrEquals(8);
});
});

const { form, errors, isValid } = createForm({
extend: validator({ suite }),
onSubmit: (values) => {
console.log('Submitting:', values);
}
});
</script>

<form use:form>
<div>
<input name="email" type="email" />
{#if $errors.email}
<span>{$errors.email[0]}</span>
{/if}
</div>

<div>
<input name="password" type="password" />
{#if $errors.password}
<span>{$errors.password[0]}</span>
{/if}
</div>

<button type="submit" disabled={!$isValid}>
Login
</button>
</form>

Custom Validation Store​

Create a reusable validation store:

// stores/validation.js
import { writable, derived } from 'svelte/store';

export function createValidationStore(suite, initialData = {}) {
const formData = writable(initialData);
const result = writable(suite.get());
const isValidating = writable(false);

function validate(fieldName) {
isValidating.set(true);

const currentData = get(formData);
suite
.afterEach(res => {
result.set(res);
isValidating.set(false);
})
.run(currentData, fieldName);
}

function validateAll() {
isValidating.set(true);

const currentData = get(formData);
suite
.afterEach(res => {
result.set(res);
isValidating.set(false);
})
.run(currentData);
}

function updateField(fieldName, value) {
formData.update(data => ({ ...data, [fieldName]: value }));
validate(fieldName);
}

function reset() {
formData.set(initialData);
result.set(suite.get());
}

const isValid = derived(result, $result => !$result.hasErrors());

return {
formData,
result,
isValidating,
isValid,
validate,
validateAll,
updateField,
reset,
};
}

Usage:

<script>
import { create, test, enforce } from 'vest';
import { createValidationStore } from './stores/validation';

const suite = create((data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
});

const {
formData,
result,
isValid,
updateField,
validateAll,
reset
} = createValidationStore(suite, { username: '', email: '' });

function handleSubmit() {
validateAll();

if ($isValid) {
console.log('Submitting:', $formData);
}
}
</script>

<form on:submit|preventDefault={handleSubmit}>
<input
value={$formData.username}
on:input={(e) => updateField('username', e.target.value)}
/>
{#if $result.hasErrors('username')}
<span>{$result.getErrors('username')[0]}</span>
{/if}

<button type="submit" disabled={!$isValid}>Submit</button>
<button type="button" on:click={reset}>Reset</button>
</form>

TypeScript Support​

Vest works great with TypeScript in Svelte:

<script lang="ts">
import { writable, type Writable } from 'svelte/store';
import { create, test, enforce, type SuiteResult } from 'vest';

interface FormData {
username: string;
email: string;
age: number;
}

const suite = create((data: Partial<FormData> = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});

test('age', 'Must be 18 or older', () => {
enforce(data.age).greaterThanOrEquals(18);
});
});

let formData: Partial<FormData> = {
username: '',
email: '',
age: undefined
};

let result: SuiteResult = suite.get();

function validateField(fieldName: keyof FormData): void {
suite.afterEach((res) => {
result = res;
}).run(formData, fieldName);
}
</script>

## Best Practices

### 1. Create Suite Outside Component

Define your validation suite in a separate file. Vest suites are stateful, so they should be treated as singletons for a given form.

```js
// validations/signupSuite.js
import { create, test, enforce } from 'vest';

export const signupSuite = create((data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotBlank();
});
// ... more tests
});
```

### 2. Debounce Expensive Validations

Use debouncing for async or expensive validations:

```svelte
<script>
import { debounce } from './utils';

const validateUsername = debounce((value) => {
suite.afterEach(setResult).run({ username: value }, 'username');
}, 500);

$: validateUsername(username);
</script>
```

### 3. Show Errors After Touch

Only show errors after a field has been touched:

```svelte
<script>
let touched = {
username: false,
email: false
};

function handleBlur(fieldName) {
touched[fieldName] = true;
}

function shouldShowError(fieldName) {
return touched[fieldName] && result.hasErrors(fieldName);
}
</script>

<input
bind:value={formData.username}
on:blur={() => handleBlur('username')}
on:input={() => validateField('username')}
/>
{#if shouldShowError('username')}
<span>{result.getErrors('username')[0]}</span>
{/if}
```

## Next Steps

- Learn about [Core Concepts](/docs/concepts) to understand Vest's architecture
- Explore [Async Tests](/docs/writing_tests/async_tests) for handling asynchronous validations
- Check out [Skip and Only](/docs/writing_your_suite/including_and_excluding/skip_and_only) for complex validation scenarios