Admin edit forms have a quiet failure mode: you open a user's record, change nothing except the role, hit save — and the API rejects you because the unchanged email "has already been taken". By you. The user you're editing. Every admin panel hits this in week one; the fix is two FormRequest rules most people learn the hard way.
sometimes: validate what's there, ignore what's not
The update request for users looks like this:
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:255'],
'email' => ['sometimes', 'string', 'email', 'max:255',
Rule::unique('users', 'email')->ignore($this->route('user'))],
'password' => ['sometimes', 'string', 'min:8'],
'role' => ['sometimes', 'string', 'in:admin,user'],
];
}
sometimes means: only run these rules if the field is present in the request. Send {role: "admin"} and the name/email/password rules never fire — absent fields pass through untouched instead of failing required or, worse, being nulled out by $request->validated().
This is the difference between partial updates done right and the classic bug where an edit wipes fields the form didn't send. The controller is one line — $user->update($request->validated()) — and it's safe precisely because validated() only contains what was sent and passed.
unique-except-self: the email that "already exists"
The email rule is the one I see copied wrong most often:
Rule::unique('users', 'email')->ignore($this->route('user'))
Plain unique:users,email compares against every row, including the one being edited — so saving an unchanged email fails validation. ->ignore() exempts the current record. $this->route('user') is the route-model-bound User instance (the users/{user} resource route provides it), and ignore() accepts the model directly — no need to pluck the id yourself.
Small detail, big consequence: without it, admins learn to "fix" the error by tweaking the email slightly. Then the user's login breaks. I've watched this happen.
Contrast with the create request
The store request is the strict sibling — everything required, because creation has no existing values to fall back on:
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'min:8'],
'role' => ['required', 'string', 'in:admin,user'],
Two deliberate asymmetries worth stealing:
-
roleis required on create butsometimeson update. A new user must have an explicit role; an edit shouldn't have to restate it. (Androleon the store request is safe here only because the entire users resource sits behindrole:adminmiddleware — an admin assigning roles is the feature.) -
Registration's password needs
confirmed; the admin's doesn't.RegisterRequesthas'password' => ['required', 'string', 'min:8', 'confirmed']— a self-service stranger must type it twice. An admin setting a password for someone else gets no confirmation field, because there is no second typing to compare against. Match the rule to the trust level of the actor.
None of these rules is clever. Together they're the difference between an admin panel you trust and one where every edit feels slightly dangerous. Validate what's sent, exempt the record you're editing, and let the request shape — create vs update — decide how strict each field is.
These are the exact FormRequests in my Laravel + React SaaS Starter Kit ($19 one-time) — Sanctum auth, role-based access, and an admin users table with validation that doesn't fight you.
Top comments (0)