Laravel Precognition: Real-Time Validation Without Duplicating Rules
Form validation is the most duplicated code in web development. You write rules in Laravel, then rewrite them in JavaScript. When the rules change, you update both — or forget and create bugs.
Laravel Precognition eliminates this by letting your frontend ask the backend to validate individual fields in real-time, reusing your existing Form Request rules.
How It Works
User types email → Frontend sends partial request → Backend validates ONLY that field
→ Returns validation result
→ Frontend shows error instantly
No full form submission. No page reload. No duplicated rules.
Installation
Backend
composer require laravel/precognition
Frontend (choose your stack)
# Vue
npm install laravel-precognition-vue
# React
npm install laravel-precognition-react
# Alpine.js
npm install laravel-precognition-alpine
Backend Setup
1. Add the Middleware
// routes/web.php
use App\Http\Controllers\RegistrationController;
Route::post('/register', [RegistrationController::class, 'store'])
->middleware('precognitive');
The precognitive middleware tells Laravel: "If this is a precognitive request, validate and return early — don't execute the controller logic."
2. Use a Form Request (You Probably Already Have One)
// app/Http/Requests/RegisterRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
class RegisterRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'username' => ['required', 'string', 'min:3', 'max:30', 'unique:users,username', 'alpha_dash'],
'password' => ['required', 'confirmed', Password::min(8)->mixedCase()->numbers()],
];
}
public function messages(): array
{
return [
'username.unique' => 'This username is already taken.',
'email.unique' => 'An account with this email already exists.',
];
}
}
3. Use It in the Controller
// app/Http/Controllers/RegistrationController.php
class RegistrationController extends Controller
{
public function store(RegisterRequest $request)
{
// This code ONLY runs on real submissions, not precognitive requests
$user = User::create($request->validated());
Auth::login($user);
return redirect('/dashboard');
}
}
That's it for the backend. Your existing Form Request works as-is.
Frontend: Vue 3
<script setup>
import { useForm } from 'laravel-precognition-vue';
const form = useForm('post', '/register', {
name: '',
email: '',
username: '',
password: '',
password_confirmation: '',
});
const submit = () => form.submit({
onSuccess: () => window.location.href = '/dashboard',
});
</script>
<template>
<form @submit.prevent="submit">
<div>
<label>Name</label>
<input
v-model="form.name"
@change="form.validate('name')"
/>
<span v-if="form.invalid('name')" class="text-red-500">
{{ form.errors.name }}
</span>
</div>
<div>
<label>Email</label>
<input
v-model="form.email"
@change="form.validate('email')"
type="email"
/>
<span v-if="form.invalid('email')" class="text-red-500">
{{ form.errors.email }}
</span>
<span v-if="form.valid('email')" class="text-green-500">
✓ Available
</span>
</div>
<div>
<label>Username</label>
<input
v-model="form.username"
@change="form.validate('username')"
/>
<span v-if="form.invalid('username')" class="text-red-500">
{{ form.errors.username }}
</span>
</div>
<div>
<label>Password</label>
<input
v-model="form.password"
@change="form.validate('password')"
type="password"
/>
<span v-if="form.invalid('password')" class="text-red-500">
{{ form.errors.password }}
</span>
</div>
<div>
<label>Confirm Password</label>
<input
v-model="form.password_confirmation"
@change="form.validate('password')"
type="password"
/>
</div>
<button :disabled="form.processing" type="submit">
Register
</button>
</form>
</template>
Key points:
@change="form.validate('email')"— triggers validation for that single field when the user leaves the inputform.invalid('email')— checks if the field has errorsform.valid('email')— checks if the field has been validated and passed- The form handles debouncing automatically
Frontend: Alpine.js
For Blade-first apps:
<form
x-data="{
form: $form('post', '/register', {
name: '',
email: '',
username: '',
password: '',
password_confirmation: '',
})
}"
@submit.prevent="form.submit()"
>
<div>
<label>Email</label>
<input
x-model="form.email"
@change="form.validate('email')"
type="email"
>
<template x-if="form.invalid('email')">
<p x-text="form.errors.email" class="text-red-500 text-sm"></p>
</template>
<template x-if="form.valid('email')">
<p class="text-green-500 text-sm">✓ Available</p>
</template>
</div>
<!-- Other fields... -->
<button :disabled="form.processing" type="submit">
Register
</button>
</form>
Debouncing & Timing
By default, Precognition debounces requests at 1500ms (typing) and 0ms (on change). Customize it:
const form = useForm('post', '/register', {
email: '',
});
// Validate while typing (with debounce)
form.setValidationTimeout(500); // 500ms debounce
<!-- Validate on input (while typing) instead of on change -->
<input
v-model="form.email"
@input="form.validate('email')"
type="email"
/>
Handling Side Effects
Sometimes validation has side effects you want to skip during precognitive requests:
class StoreOrderRequest extends FormRequest
{
public function rules(): array
{
return [
'coupon_code' => ['sometimes', 'string', 'exists:coupons,code'],
'items' => ['required', 'array', 'min:1'],
];
}
protected function prepareForValidation(): void
{
// Skip coupon lookup during precognitive validation
if ($this->isPrecognitive()) {
return;
}
// Only on real submission
$this->merge([
'discount' => $this->calculateDiscount(),
]);
}
}
Validating Specific Fields Only
Precognition only validates the fields the frontend asks about. But sometimes you need conditional logic:
public function rules(): array
{
return [
'type' => ['required', 'in:personal,business'],
'company_name' => ['required_if:type,business', 'string', 'max:255'],
'tax_id' => ['required_if:type,business', 'string'],
'name' => ['required', 'string', 'max:255'],
];
}
When the frontend validates type, Precognition knows to also validate dependent fields.
With Inertia.js
Precognition works seamlessly with Inertia:
<script setup>
import { useForm } from 'laravel-precognition-vue-inertia';
const form = useForm('post', '/posts', {
title: '',
body: '',
category_id: null,
});
const submit = () => form.submit({
preserveScroll: true,
onSuccess: () => form.reset(),
});
</script>
File Uploads
Precognition supports file validation too:
public function rules(): array
{
return [
'avatar' => ['required', 'image', 'max:2048', 'dimensions:min_width=100,min_height=100'],
];
}
<input
type="file"
@change="e => {
form.avatar = e.target.files[0];
form.validate('avatar');
}"
/>
<span v-if="form.invalid('avatar')">{{ form.errors.avatar }}</span>
The file is sent to the backend for validation — size, dimensions, MIME type — all checked server-side.
Performance Considerations
Each field validation is an HTTP request. Optimize:
- Use
@changenot@inputfor expensive rules (likeunique) - Debounce appropriately — 500ms for typing, 0ms for dropdowns
- Group related validations:
// Validate multiple fields at once
form.validate(['password', 'password_confirmation']);
- Cache database checks when possible:
public function rules(): array
{
return [
// Use rate limiting on unique checks if needed
'email' => [
'required',
'email',
Rule::unique('users')->ignore($this->user()?->id),
],
];
}
Testing
public function test_precognitive_validation_for_email(): void
{
User::factory()->create(['email' => 'taken@example.com']);
$this->post('/register', [
'email' => 'taken@example.com',
], [
'Precognition' => 'true',
'Precognition-Validate-Only' => 'email',
])->assertStatus(422)
->assertJsonValidationErrors('email');
}
public function test_precognitive_success(): void
{
$this->post('/register', [
'email' => 'new@example.com',
], [
'Precognition' => 'true',
'Precognition-Validate-Only' => 'email',
])->assertStatus(204); // No errors
}
Conclusion
Laravel Precognition gives you real-time form validation with zero rule duplication. Your Form Requests are the single source of truth.
When to use it:
- Registration/signup forms with uniqueness checks
- Multi-step forms where each step needs validation
- Complex forms with conditional rules
- Any form where instant feedback matters
When not to use it:
- Simple forms with only frontend-checkable rules (required, min length)
- Forms that rarely fail validation
- High-traffic forms where server load is a concern (use client-side validation instead)