"Validate at the edges" is one of those phrases everyone agrees with and nobody finishes. I wrote before about why TypeScript types evaporate at runtime and why the boundary needs its own check. This post is the part that comes after: what a good validation failure actually looks like.
Because validation that fails badly is worse than no validation. It trains people to click through, to wrap things in try-catch and move on, to treat the error as noise. The failure path is the feature.
Fail at parse time, not at render time
A component that receives garbage will happily render garbage. By the time the wrong value is on screen, you have lost the chance to make a decision about it. So the rule I follow in Nuxt projects is simple: external data gets parsed the moment it enters, and the parse decides one of three things - reject, repair, or default.
The key word is "decides". A silent fallback is not a decision, it is a hidden bug with good manners. Whichever branch fires, it gets logged. If my API suddenly starts sending dates in a new format, I want to see "repaired 14 records today" somewhere, not discover it in a screenshot from a user.
(Illustrative example: an API returns {"price": "12.50"} where the schema expects a number. Repair means coercing the string and logging it. Default means falling back to zero and logging louder. Reject means the request fails visibly. All three are defensible. Silently doing one of them is not.)
Error messages are a product surface
"Invalid input" is where validation goes to die. The person filling the form cannot act on it, the developer reading the log cannot act on it, and after the tenth time everyone learns to ignore it.
Zod (I'm on v4, currently 4.6.x) gives you an issue list with a path and a code for every failure. Turning that list into per-field messages a human can use is unglamorous work, and it is most of the value. "Swiss postcode must be 4 digits" gets the form fixed. "Invalid string" gets the form abandoned.
The same applies to logs. An issue with its path is a bug you can find. A toast saying "something went wrong" is archaeology.
Don't trust your own API either
This is the one people skip. You control the backend, so validating its responses feels paranoid. But backends change: a migration renames a field, a new colleague changes a serializer, a cache serves a stale shape. The frontend that validated the response gets a clean, logged failure at the boundary. The frontend that trusted the response gets a blank screen and a mystery.
The same parse that guards the form guards the fetch. One schema, two directions.
The point
Validation is not about purity or being right. It is about making bad data fail early, loudly, and with a message someone can act on. If your validation fails quietly, it is not validation - it is documentation of what you hoped the data looked like.
Write the schema. Decide reject, repair, or default. Log which one fired. Say what was expected, in words a person understands. That is the whole job, and it is worth doing well.
Examples in this post are illustrative. Zod version verified against the npm registry at writing time.
Top comments (0)