Forms connect your application to its users. They also expose every weak point in your stack.
A missing field needs a clear message. A large upload needs visible progress. A failed request must restore the right state. With Laravel, Vue, and Inertia 3.x, these concerns fit into one consistent workflow.
Laravel handles validation on the server. Vue renders the form state. Inertia connects both sides without forcing you to build a separate client-side validation system.
That makes the stack a strong choice for teams using a modern PHP web framework and practical PHP developer tools.
The Form Contract: Laravel Validates, Inertia Translates
Inertia forms use normal Laravel requests. Your controller can validate input with $request->validate() or a dedicated Form Request.
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email'],
'avatar' => ['nullable', 'image', 'max:2048'],
]);
$path = $request->file('avatar')?->store('avatars', 'public');
User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'avatar' => $path,
]);
return to_route('users.index');
}
When validation fails, Laravel throws a ValidationException. In a standard Inertia flow, Laravel redirects back with the validation messages. Inertia then exposes those messages through the form’s errors object.
When validation succeeds, redirect to the next page or route. You usually do not need to inspect a JSON response manually.
The Laravel validation documentation covers the complete server-side lifecycle, including Form Requests, error bags, nested input, and file rules.
useForm: One Reactive Object for Every State
The useForm helper manages field values and submission state together.
<script setup>
import { useForm } from '@inertiajs/vue3'
const form = useForm({
name: '',
email: '',
avatar: null,
})
function submit() {
form.post('/users')
}
</script>
<template>
<form @submit.prevent="submit">
<label for="name">Name</label>
<input id="name" v-model="form.name" type="text">
<p v-if="form.errors.name">
{{ form.errors.name }}
</p>
<label for="email">Email</label>
<input id="email" v-model="form.email" type="email">
<p v-if="form.errors.email">
{{ form.errors.email }}
</p>
<label for="avatar">Avatar</label>
<input
id="avatar"
type="file"
@input="form.avatar = $event.target.files[0]"
>
<p v-if="form.errors.avatar">
{{ form.errors.avatar }}
</p>
<button type="submit" :disabled="form.processing">
{{ form.processing ? 'Saving…' : 'Save user' }}
</button>
</form>
</template>
The important properties are straightforward:
-
form.processingtracks the active request. -
form.errorscontains server-side validation messages. -
form.hasErrorsreports whether any errors exist. -
form.progresscontains upload progress. -
form.wasSuccessfultracks a completed submission. -
form.recentlySuccessfulsupports temporary success messages. -
form.isDirtyshows whether the user changed any values.
This keeps the template close to the user experience. You do not need separate loading, errors, and success state for every form.

Server-Side Validation Should Remain the Source of Truth
Client-side checks can improve feedback. They should not replace server-side validation.
The browser can check whether an email looks valid. Only the server can confirm that the address is unique. The browser can inspect a file extension. Laravel can inspect the uploaded file’s content and MIME type.
For larger forms, use a Form Request.
php artisan make:request StoreUserRequest
Then define the rules in one place:
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\File;
class StoreUserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'unique:users,email'],
'avatar' => [
'nullable',
File::image()->max('5mb'),
],
];
}
}
Your controller becomes smaller:
public function store(StoreUserRequest $request): RedirectResponse
{
$validated = $request->validated();
// Store the validated user data...
return to_route('users.index');
}
Laravel’s file validation rules include file, image, mimes, mimetypes, max, min, and image dimensions. The fluent File rule builder makes size and type constraints easier to read.
Do not validate only the filename supplied by the browser. A filename is user input.
Error Props: Inline Messages and Form Summaries
Most forms should display an error beside the field that needs attention.
<div>
<label for="email">Email address</label>
<input
id="email"
v-model="form.email"
type="email"
:aria-invalid="Boolean(form.errors.email)"
aria-describedby="email-error"
>
<p
v-if="form.errors.email"
id="email-error"
role="alert"
>
{{ form.errors.email }}
</p>
</div>
For long forms, add a summary at the top.
<div v-if="form.hasErrors" role="alert">
Please review the highlighted fields before submitting.
</div>
Inertia also makes validation errors available through page props. The useForm helper is usually the cleanest option for a submitted form, but shared layouts and custom components can read the page-level error props when needed.
Multiple forms on one page deserve separate error bags. For example, a profile form and a password form may both contain an email or password field. Use Laravel’s named bags and Inertia’s error bag options to keep their messages separate.
You can also clear errors after a field changes:
form.clearErrors('email')
Or reset the form and its messages together:
form.resetAndClearErrors()
The Inertia forms documentation includes additional helpers for resetting fields, setting defaults, remembering state, and manually setting errors.
File Uploads: Let Inertia Build FormData
Inertia automatically converts requests containing files into FormData. You do not need to create a FormData object by hand.
<input
type="file"
@input="form.avatar = $event.target.files[0]"
>
Then submit the form normally:
form.post('/users')
If the request should always use multipart encoding, pass forceFormData.
form.post('/users', {
forceFormData: true,
})
This is useful when a form sometimes contains a file and sometimes does not. It also makes the request format explicit.
For updates that include files, use Laravel’s method spoofing pattern. Multipart PUT and PATCH requests can be inconsistent across server environments.
form
.transform((data) => ({
...data,
_method: 'put',
}))
.post(`/users/${user.id}`, {
forceFormData: true,
})
Laravel receives the request as a PUT update while the browser sends a multipart POST.

Progress: Show the Upload Is Working
A large upload can take several seconds. Without feedback, users often click the button again or assume the application failed.
Inertia exposes upload progress through form.progress.
<div v-if="form.progress">
<progress
:value="form.progress.percentage"
max="100"
>
{{ form.progress.percentage }}%
</progress>
<span>{{ form.progress.percentage }}% uploaded</span>
</div>
Disable the submit action while the request is active:
<button type="submit" :disabled="form.processing">
{{ form.processing ? 'Uploading…' : 'Upload document' }}
</button>
You can also let the <Form> component add the inert attribute while processing. That prevents users from changing fields during a request.
<Form
action="/documents"
method="post"
disable-while-processing
>
<!-- Fields -->
</Form>
This is one of the practical advantages of Inertia 3.x. Its built-in XHR client handles requests and progress without requiring Axios for normal form submissions.
Optimistic Updates: Fast UI, Safe Rollback
Optimistic updates work well when the result is predictable.
A like counter, bookmark toggle, or new list item can appear immediately. If the request fails, Inertia restores the previous state automatically.
<script setup>
import { useForm } from '@inertiajs/vue3'
const props = defineProps({
posts: Array,
})
const form = useForm({
title: '',
})
function save() {
form
.optimistic((pageProps) => ({
posts: [
...pageProps.posts,
{
title: form.title,
pending: true,
},
],
}))
.post('/posts')
}
</script>
Optimistic state rolls back on validation errors, server errors, and interrupted visits. Inertia also tracks concurrent optimistic requests independently.
Use optimistic updates carefully with file uploads. Do not display a permanent file URL before the server stores the file. You can show a local preview while uploading, but let the server response establish the final path, identifier, and metadata.
That separation protects data integrity while keeping simple interactions responsive.
When the Form Becomes an API
Inertia is ideal when Laravel renders the page contract and Vue handles the interaction layer.
Some features need a standalone HTTP request instead. Search suggestions, background uploads, and external integrations may not need a full page visit. Inertia 3.x provides useHttp for this case.
If you need to build a REST API with PHP, Laravel still provides the routing, validation, authentication, and response tools you need. The form principles remain the same:
- Validate untrusted input on the server.
- Return structured errors.
- Show field-level feedback.
- Track processing and progress.
- Roll back speculative state when a request fails.
Laravel’s ecosystem gives you these building blocks without forcing you to reinvent the form lifecycle.
A Reliable Form Checklist
Before shipping a Laravel, Vue, and Inertia form, check the following:
- Does Laravel validate every required field?
- Are optional fields marked
nullablewhere needed? - Are file types and sizes validated on the server?
- Does every important field render its own error?
- Is there a form-level error summary?
- Is the submit button disabled while processing?
- Does an upload show progress?
- Does an update with a file use method spoofing?
- Are multiple forms separated with error bags?
- Are optimistic updates limited to safe, reversible changes?
- Does the successful response establish the final server state?
Forms do not need separate logic for every failure mode. Give Laravel responsibility for correctness, Vue responsibility for presentation, and Inertia responsibility for connecting the two.
That division keeps forms predictable as your application grows.