EMZETT.
Login

Validating forms with Zod and schemas: Checking inputs safely

In short: User input is never trustworthy; with schema libraries such as Zod you check form, type and limits once centrally, in the browser for convenience and on the server for security.

In more detail: Everything that comes from outside (forms, URL parameters, API requests, cookies) can be wrong or malicious. Validation ensures that data matches expectation before the program uses it. TypeScript checks only at development time; at runtime you need checking code.

In Depth

Zod in brief

import { z } from "zod";
 
const Signup = z.object({
  email: z.string().email("Invalid email")
  age: z.coerce.number().int().min(13).max(120)
  password: z.string().min(12)
  terms: z.literal("on", { message: "Please agree" })
});
 
const r = Signup.safeParse(Object.fromEntries(formData));
if (!r.success) return { errors: r.error.flatten().fieldErrors };
const data = r.data; // cleanly typed

The schema is the source of truth: type Signup = z.infer<typeof Signup> produces the TypeScript type. Similar: Valibot, Yup, ArkType, Joi, JSON Schema.

Where to check?

  • In the browser: quick feedback (HTML attributes required, type=email, pattern, plus schema). Convenience only, easy to bypass.
  • Always on the server too (API route, server action): this is where security lies. Never trust that the browser has checked.
  • Database: constraints (NOT NULL, UNIQUE, CHECK, foreign keys) as last defence.
  • Output: escape when displaying (XSS), not “sanitise and hope”.

Good rules

  • Allow list instead of deny list: allow known good values (enum, pattern) instead of searching for bad ones.
  • Set limits (length, size, count, value range), also for file uploads (check type, size, content).
  • Convert types deliberately (coerce), trim whitespace.
  • Error messages helpful but without internal details.
  • Reject/remove unknown fields (prevent mass assignment: the user must not send isAdmin: true along).
  • Character encoding and normalisation (lower-case email).
  • Rate limiting and CAPTCHA/honeypot against spam.

Interplay

With react-hook-form (+ zodResolver) or server actions (useActionState) you get forms with per-field error display. Accessibility: link errors to the field (aria-describedby, aria-invalid), labels, focus on the first faulty field. Privacy: collect only necessary fields. Test validation with unit tests (valid and invalid examples, edge cases).

See also: SQL injection, XSS, Rate limiting, CAPTCHA, TypeScript