HomeProjectsBlogContact

Input Validation: Client-Side vs Server-Side (You Need Both)

Client-side validation makes forms pleasant; server-side validation keeps your app safe. How to validate properly with schemas, share rules between frontend and backend, and avoid common gaps.

Input Validation: Client-Side vs Server-Side (You Need Both)

Every app that accepts input, which is every app, needs validation. Where it happens matters a lot, and the most common mistake is treating client-side checks as if they protect the server. They don't. Here's how to do both properly.

Client-side validation is for users

Validation in the browser gives instant feedback: "That email doesn't look right," "Password must be at least 12 characters." It saves users from submitting a form only to be told what went wrong.

HTML has a lot built in:

<input type="email" required>
<input type="number" min="1" max="10">
<input type="text" minlength="3" maxlength="40" pattern="[a-z0-9-]+">

Plus JavaScript for richer rules and better messages.

But anyone can bypass the browser entirely: disable JavaScript, edit the HTML, or send requests with curl or a script. Client-side validation is a convenience, never a security measure.

Server-side validation is for safety

Your server must validate every input as if the frontend didn't exist, because for an attacker, it doesn't. That includes:

Use a schema

Hand-written if checks get inconsistent quickly. A schema library declares the rules in one place and handles the details:

import { z } from 'zod'

const SignupSchema = z.object({
  email: z.string().trim().toLowerCase().email(),
  password: z.string().min(12).max(200),
  displayName: z.string().trim().min(1).max(40).optional(),
  plan: z.enum(['free', 'pro']),
})

app.post('/api/signup', async (req, res) => {
  const result = SignupSchema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({ error: 'Validation failed', fields: result.error.flatten().fieldErrors })
  }
  const data = result.data   // trimmed, normalised, typed
  // ...
})

Equivalent tools exist everywhere: Pydantic in Python, Laravel's validation rules, Joi, Yup and Valibot in JavaScript, and so on.

Share the rules

Define the schema once and use it on both sides. The frontend uses it to show friendly errors; the backend uses it to enforce them. With TypeScript, the same schema also produces your types. (Is TypeScript worth it?)

That eliminates the classic bug where the frontend allows 50 characters and the backend allows 40.

Validate by allow-list

Describe what's allowed, not what's forbidden.

  • Accept status only from a fixed set of values.
  • Accept only known fields; strip or reject unexpected ones. This prevents mass assignment, where a user adds "role": "admin" to their profile update and an ORM saves it.
  • Set maximum lengths on every string and limits on every number and array. Unbounded input is a denial-of-service waiting to happen.

Normalise before validating

  • Trim whitespace.
  • Lowercase emails before checking uniqueness.
  • Parse numbers and dates into real types.
  • Normalise Unicode where it matters, like usernames.

Validation is not escaping

These are different jobs:

  • Validation decides whether input is acceptable.
  • Escaping or encoding makes data safe for a specific output context: HTML, SQL, a shell command, a URL.

Even perfectly valid input, like a comment containing <script>, can be dangerous if output unsafely. Use parameterized queries for SQL and your framework's auto-escaping for HTML. (Security checklist.)

Business rules belong on the server too

Some validation needs data only the server has:

  • Is this email already registered?
  • Is this coupon valid and unexpired?
  • Is there enough stock?
  • Does this user own the record they're editing?

These checks must happen server-side, ideally backed by database constraints (unique indexes, foreign keys, check constraints) as the final safety net.

Return helpful errors

Respond with a 400 (or 422) and field-level messages the frontend can display next to the right inputs:

{
  "error": "Validation failed",
  "fields": {
    "email": ["Enter a valid email address"],
    "password": ["Must be at least 12 characters"]
  }
}

(Consistent API error formats.) Keep messages human and specific, and never echo back raw internal errors.

A good form experience

  • Validate on blur or submit, not on every keystroke, so users aren't scolded mid-typing.
  • Put error messages next to fields, in text, not just colour. (Accessible forms.)
  • Preserve what users typed when showing errors.
  • Disable the submit button while submitting to prevent double posts.

FAQ

Is client-side validation enough?

No. Client-side validation can be bypassed easily. It improves user experience, but the server must validate every input independently.

What is the best way to validate input on the server?

Use a schema validation library to define allowed fields, types, formats and limits in one place, reject or strip unknown fields, and back critical rules with database constraints.

What's the difference between validation and sanitization?

Validation checks whether input meets your rules and rejects it if not. Sanitization or escaping transforms data to make it safe in a particular output context, like HTML or SQL.

Keep reading

More from the workbench

All articles →