Input validation is where the product says what it believes.
It is easy to treat validation as defensive plumbing: check the type, trim the string, reject nonsense, move on. That is true, but incomplete. Validation also defines the product’s shape. It decides what counts as a valid name, which dates make sense, whether a user can submit partial data, which files are acceptable, and how much ambiguity the system will tolerate.
Those decisions are not neutral. They show up as user experience, security posture, analytics quality, and support tickets.
If the product accepts garbage, the rest of the system pays for it. If it rejects legitimate input, users pay for it. The validation layer is where that tradeoff becomes code.
validation should live at the boundary
The most important validation happens where untrusted input enters the system.
Client-side validation is useful for fast feedback. Server-side validation is mandatory. The server owns the boundary because clients can be bypassed, old, broken, automated, or malicious.
I want request handlers to turn unknown input into known shapes before business logic sees it:
type CreateWorkspaceInput = {
name: string
slug: string
billingEmail?: string
}
The handler should validate length, allowed characters, required fields, and cross-field rules before creating anything. The rest of the code should not wonder whether slug is empty, whether billingEmail is a valid email string, or whether extra fields were smuggled into the request.
Validation is not only about rejecting attacks. It is about creating trustworthy internal data.
schemas are product artifacts
A schema is a product artifact, even when it lives in code.
Consider a workspace slug. The rules might say:
lowercase letters, numbers, and hyphens
3 to 40 characters
must start with a letter
cannot end with a hyphen
reserved words blocked
unique within the system
Those rules create product behavior. They decide whether acme-inc, acme_2025, 123-acme, or admin can exist. They affect URLs, support scripts, screenshots, docs, and user expectations.
If product, design, and engineering have different mental models of the slug, the validation error becomes the first place the disagreement appears.
The schema should be readable enough that the product can review it. It should also have tests because validation rules tend to grow quietly.
error messages are part of validation
A validation rule without a useful error message is only half finished.
Bad:
Invalid value.
Better:
Use 3 to 40 lowercase letters, numbers, or hyphens. Start with a letter.
That message teaches the rule. It also reduces support load and retry frustration.
Error messages should avoid leaking sensitive information. There is a difference between “that email is not a valid address” and “that email belongs to an existing account.” The second may be useful in an authenticated admin flow and dangerous in a public signup form.
So error copy needs context. The same validation rule may need different messages depending on who is asking and what they are allowed to know.
normalization is a decision
Validation and normalization are related but different.
Validation asks whether input is acceptable. Normalization changes input into a canonical form. Trimming whitespace, lowercasing emails, parsing dates, stripping formatting from phone numbers, and converting Unicode forms are all normalization decisions.
Normalization can be helpful. It can also surprise users.
Lowercasing an email is usually fine. Lowercasing a display name is not. Trimming a project name might be fine. Collapsing internal whitespace might change meaning. Parsing 03/04/2025 without a locale rule is asking for pain.
I like making normalization explicit:
email:
trim surrounding whitespace
lowercase domain
preserve local-part casing for display
workspace slug:
lowercase
replace spaces with hyphens
reject other punctuation
display name:
trim surrounding whitespace
preserve casing and internal spaces
That is product design. It decides what the user meant.
partial input deserves a state
Some input is invalid because it is unfinished.
That matters in forms and APIs. A user typing a date field may briefly enter 2025-. That is not a valid date, but it is a valid intermediate state. A draft object may be missing required publish fields. A bulk import may have some valid rows and some invalid rows.
The product should distinguish invalid final input from incomplete work.
For APIs, that might mean separate draft and publish schemas. For UI, it might mean delaying errors until blur or submit. For imports, it might mean accepting valid rows while returning row-level failures for the rest.
If every validation failure is treated the same, the product becomes either too strict while users are working or too loose when data becomes official.
validation needs ownership
Validation rules drift when nobody owns them.
A billing rule changes. A file size limit gets raised. A username format from the old app remains in the API. A field becomes optional in the UI but required on the server. A new client starts sending values the old validation rejects.
The fix is not only better code reuse. It is ownership. Someone has to know which rule is the source of truth and which surfaces depend on it.
For shared rules, I want a small map:
rule: workspace slug
owner: platform
used by:
- signup form
- workspace settings
- invite acceptance
- public URL router
server source: src/validation/workspace.ts
client helper: src/forms/workspaceSlug.ts
That makes it harder for one surface to quietly fork the product definition.
ai makes validation more important
AI systems make validation more valuable because generated output can look structured while being wrong.
If a model writes JSON, validate it. If it proposes tool arguments, validate them. If it extracts entities, validate the shape before writing them into state. If it generates a migration, validate the target schema and run checks before applying anything.
The model is not the boundary. The validator is.
For tool calls, I want narrow schemas and clear rejection:
{
"tool": "create_ticket",
"args": {
"title": "Parser fails on quoted commas",
"priority": "high",
"owner": "platform"
}
}
If priority can only be low, medium, or high, reject anything else. Do not let the model invent urgent-ish. If owner must be an existing team ID, resolve it before mutation. A helpful model can still produce invalid operational data.
validation is a contract with the future
Good validation makes future code simpler.
Once the boundary is enforced, internal code can trust more. Analytics get cleaner. Support sees fewer impossible states. Security gets fewer weird inputs reaching deeper layers. Product gets clearer language around what the system accepts.
Bad validation spreads uncertainty. Every function asks whether the value is real. Every UI handles a state that should never exist. Every migration discovers old rows that violate the current rules.
The annoying part is that validation feels small when you write it. It is a string length, a regex, an enum, a date comparison. But those small rules define the product’s reality.
That is why they deserve design-level attention.
Related posts

About Jeremy London
Engineering leader and builder in Denver. I write about AI platforms, agents, security, reliability, homelab infrastructure, and the parts of engineering work that have to survive production.