React
Server-side validation is mandatory
Client-side validation answers fast, server-side validation decides: anyone can send a request without the form, so every field is rechecked.
Key points
- The client can be bypassed: anyone can send the request body without opening the form.
- One shared Zod schema avoids writing the same rules in the browser and in the route.
- Uniqueness and plan limits stay database constraints, not schema rules.
Client-side validation answers quickly; server-side validation is the only one that decides, because anyone can send a request without touching the form.
Who can skip the form
A form in the browser is just another client: you can turn JavaScript off, edit an attribute, write a fetch by hand or repeat the request ten times. The server receives bytes, so it must check every field it uses, even when the client already said it was fine.
One schema, two runs
import { z } from "zod";
const Signup = z.object({
email: z.string().email(),
password: z.string().min(12),
});
// Nel browser: feedback immediato.
Signup.safeParse(values);
// Sul server: stessa funzione, stessa verità.
const parsed = Signup.safeParse(await request.json());
if (!parsed.success) {
return Response.json({ errors: parsed.error.flatten() }, { status: 422 });
}
Zod, released under the MIT licence, returns a typed result instead of throwing: the same schema runs in the browser and in the route, and the error message is not written twice.
What the server cannot know
- No form context: the server reads the request body, not the component, so every field has to be named and checked.
- Forgeable headers: a header that looks like it came from the browser may have been written by hand.
- Duplicate requests: two identical submissions arrive together, and the second must not create a second record.
The proof, in one command
curl -i -X POST http://localhost:3000/api/signup \
-H "Content-Type: application/json" \
-d '{"email":"nope","password":"1"}'
The command skips the form and prints the raw response: if the route accepts that body, the problem is on the server, not in the client.
Live records in the catalogue
Three forms where server validation matters: sign-in-form, auth-form and form-layout-01. To browse by category start from 111,985 components.
The limitation
A shared schema checks shape, not business rules: unique email, breached passwords and plan limits stay as queries and constraints in the database. Two concurrent requests both pass the schema and collide on the unique index, so the constraint belongs in the database as well.
Where to continue
The full flow is in validation with Server Actions; the measured comparison of libraries is in React form libraries; the choice between a server action and an API is in Server Actions vs API.
Frequently asked questions
Is client-side validation useless?
No, it shows an error before the network round trip. It is not a defence: the server must repeat every check it uses.
Do I have to duplicate the rules on the server?
No, one schema imported by both sides is better. When the two copies drift apart, the difference is a bug.
Which status code should a validation error return?
422 usually. The 400 stays for a malformed request, the 422 for a readable but invalid body.
How do you stop double submissions?
With an idempotency key per request and a unique index in the database, not with a disabled button.
Is Zod required?
No. Any shared schema works; Zod ships under the MIT licence and returns a typed result.