Every web app has forms, and every form has rules. An email must look like an email. A phone number needs the right length. A price cannot be negative. The trouble starts when those rules live in three places: the frontend form, the backend API and the database. Over time they drift apart. The form allows a 100 character name, the API accepts 50, and the database column holds 40. Users see confusing errors, bugs slip through and developers waste hours reconciling rules.
A shared validation schema fixes this by defining the rules once and reusing them everywhere. In the TypeScript world, Zod is a popular library for doing exactly that. This guide explains the idea in plain language, shows how to structure it in a real project, and lists the pitfalls that catch teams out.
Imagine a signup form for a SaaS product. The frontend developer adds checks so the email must be valid and the password must have at least eight characters. Later the backend developer writes an API route and adds similar checks, but requires ten characters because security asked for it. Nobody updates the form. Now users type a nine character password, the form says it is fine, the server rejects it, and the user sees a vague "something went wrong" message.
This kind of mismatch causes real business damage. Signup drop-offs rise, support tickets grow, and engineers lose trust in the code. It is also a security risk, because rules that are only enforced on the client are not rules at all.
A schema is a description of valid data. With Zod you write something like "an object with a name that is a string between 2 and 60 characters, an email that is a valid address, and an age that is an optional whole number above 17". From that one description you get three things:
Because the type comes from the schema, the compiler warns you whenever the data shape changes and some code is out of date. That alone removes a whole class of bugs.
Consider a B2B ordering portal where customers submit a purchase request. The form collects company name, contact email, a list of line items with quantity and unit, and an optional delivery note. The rules are simple but important: quantity must be a positive whole number, the delivery note is capped in length, and at least one line item is required.
The team defines an order schema once in a shared folder. The React form uses it to validate as the user types and to show messages beside each field. The API route uses the same schema to validate the request body before touching the database. A background job that imports orders from a spreadsheet uses it as well, so bad rows are rejected with the same messages. When the business later adds a rule that the delivery note must be under 200 characters, one line changes and all three entry points update together. This is an illustrative scenario, but it matches how shared schemas pay off in practice.
Form inputs and query strings arrive as text. Use coercion helpers so "42" becomes the number 42 before validation, and decide how to treat empty strings. An empty price field should not silently become zero.
Some rules involve more than one field, such as "end date must be after start date" or "confirm password must match password". Use refinement functions on the object schema and attach the error to the field the user should fix.
Be explicit. A field that may be missing, a field that may be null and a field that may be an empty string are different situations. Decide which one your database and API use and keep the schema aligned with it.
If your product serves multiple languages, keep message text separate from rules. Return error codes from the schema and translate them in the interface. This also keeps your API responses stable for mobile clients. For related thinking on API stability, see our guide on API versioning for mobile apps and backward compatibility.
Think of validation as layers, each with a different job:
A shared schema covers the first two layers well. Do not try to squeeze lookups and database checks into it. Keep the schema pure and fast, and let services handle rules that require context.
The same schema thinking also helps with file uploads, webhooks and third party responses. Anything that crosses a trust boundary deserves validation. If your app accepts files, pair it with the practices in our guide to secure file uploads with presigned URLs.
Zod is not the only option. Other TypeScript libraries offer similar ideas, and some API frameworks generate validation from OpenAPI specs or database models. The best choice depends on your stack. If your backend is not TypeScript, you might define rules in a language neutral format such as JSON Schema and generate checks for each side. The principle stays the same: define the contract once, and enforce it at every boundary.
Shared validation schemas are a small investment with a large return. They replace scattered, inconsistent checks with a single contract that your forms, APIs and jobs all respect. Start with your most important form, such as signup or checkout, move its rules into a schema, validate on both sides, and watch the support noise drop.
If you are planning a new product or cleaning up an existing codebase, our team can help you design a maintainable foundation. Learn more about our web development services and how we build reliable, typed web applications for startups and SMEs.