skip to content

Would you define a form's validation rules once and share them with the server, or keep two sets, and what does each choice cost?

level: principalimportance: should knowfreq 44%

answer

  1. two sets, one gets missed
  2. shareable means expressible as data
  3. some rules need state the client lacks
  4. deployed clients carry old copies
  5. the service always re-checks

basics

~20 s

Share one declarative definition for the constraints both sides can express, and accept the coupling. Rules needing data only the server holds stay there. The server validates again regardless; the client's verdict is a preview.

solid answer

~50 s

The argument for one shared definition is drift: two hand-maintained rule sets diverge on the first change that only one side gets, and the failure is a user blocked by a client rule the server dropped, or a submission the client cleared and the server refuses. A single declarative schema, consumed by both, removes that class of bug for the rules that are expressible on both sides - presence, shape, length, ranges, comparisons within one payload. What cannot be shared is any rule needing state the client does not have: uniqueness, permissions, cross-record invariants, anything about concurrent changes. The cost of sharing is coupling: one definition but two deploys, and older clients hold older copies. So the client's pass is a fast preview, the server's is the decision, and the form must be able to receive a rejection for a field it believed was fine.

go deeper

for a junior

Remember the fixed point: the service checks the payload no matter what the form checked, and the form's rules exist to give the user faster feedback.

for a middle

Explain what a shared declarative definition removes - drift between two rule sets - and which rules cannot travel to the client because they need data it does not have.

for a senior

Discuss the operational edges: which side deploys first when a rule tightens, how per-field rejections are presented for a field the client cleared, and what run-time rule fetching costs.

for a principal

Own the decision and its blast radius: what belongs in the shared contract, how it is versioned across independently released clients, and the standing rule that the service is the authority.

Every form of any size faces this question once: are the rules written twice - once for the interface, once for the service - or written once and read by both? It is a judgment call with real tradeoffs, which is why it comes up in design discussions rather than in code review. ## Why the duplicate set rots Two independent rule sets are two places to change and one of them will be missed. The two failure directions are asymmetric but both bad: - **The client is stricter than the server.** Users are blocked from submitting values the service would happily accept. Nobody files a bug, because the form looks like it is working; the business simply loses the submissions. - **The client is laxer than the server.** The form says the value is fine and the service refuses it. If the form cannot present that refusal well, the user is stuck at a green field with a failing submit - the worst experience in this whole area. Drift also hurts the messages: the same rule worded differently in two places means the user sees one phrasing while typing and another after submitting. ## What a shared definition buys, and what it costs | Approach | Buys | Costs | |---|---|---| | One declarative definition, consumed by both | No drift for shared rules; one place to change; consistent messages | Coupling between interface and service releases; a lowest-common-denominator rule language; version skew across deployed clients | | Two hand-written sets | Each side free to use its own idioms and add its own rules | Drift on the first unmatched change; duplicated wording; silent over-strictness | | Server-described rules fetched at run time | Always current; no client redeploy for a rule change | A round trip before the form can judge; a rule language to interpret; behaviour before the description arrives | The shared definition has to be *data*, not code, to travel: a declarative description of constraints, from which each side builds its checks. The moment a rule needs arbitrary logic it stops being portable, which is the natural boundary of what should be shared at all. ## What is not shareable Three categories stay server-side by nature: 1. **Rules over state the client cannot see** - uniqueness across all records, quota and balance checks, anything involving other users' data. 2. **Rules that depend on who is asking** - a field one role may set and another may not. 3. **Rules about concurrency** - the value was free when checked and is taken now. Even a perfectly shared rule set cannot make a client-side verdict current. And one category stays client-side: the *timing and presentation* policy - when a rule runs, when its verdict may show, how the message reads in the user's language. A shared schema describes constraints, not the moment a user should be told about them. ## Version skew is the interesting part Once the definition ships inside the client, deployed clients carry copies of it at different ages. Consequences a lead has to plan for: - The service must remain the authority, and must keep returning machine-readable per-field verdicts, because some client will always be judging by an older rule set. - Loosening a rule is safe to deploy service-first; tightening it will be enforced before older clients warn about it, so those users meet a rejection with no client-side preview. That is acceptable if the rejection is presented well, and it usually motivates shipping the schema change to clients first. - Fetching the definition at run time removes skew but adds a dependency the form needs before it can validate, plus a rule interpreter, plus a decision about what to do when the fetch fails. ## The rule that does not change Whatever is shared, the service validates the payload it receives. That is not redundancy to be optimised away - the client is a participant, not a gatekeeper, and a request can arrive without ever having passed through the form. The timing consequence for the form is concrete: treat every local verdict as provisional, keep the submission path capable of receiving a per-field rejection for a field the client cleared, and never let the presence of client rules become a reason to weaken the service's own. ## A practical default For one product with one team shipping both sides, share a declarative definition for the mechanical constraints, keep the server-only rules where they belong, and treat the client copy as an accelerator for the user rather than as the verdict. For a public service with independently released clients, publish the constraint description as part of the interface contract and version it, so the drift you cannot prevent at least becomes visible.

  • If the rules are shared and provably identical, can the service skip validating the payload?
    No. The service receives requests that never went through the form, and a shared definition says nothing about what actually reached the wire. The shared schema removes drift between the two rule sets; it does not turn the client into a gatekeeper. The service validates every payload it accepts, and the client copy exists to shorten the user's feedback loop.
  • Which direction of drift is worse: a client stricter than the server, or laxer?
    Stricter is quieter and often more costly - users are blocked from valid submissions and nothing reports it, because the form appears to work. Laxer is louder: the user is refused after submitting, which is visible and fixable if the form presents per-field rejections well. Both are drift, but the silent one tends to survive longer in production.
  • What happens when a rule is tightened on the server while older clients still hold the looser copy?
    Those users can compose a value their form calls valid and have it rejected on submit. The form must therefore be able to render a per-field rejection at any time. To avoid the surprise, ship the tightened definition to clients before enforcing it, or fetch the definition at run time and accept the extra dependency that introduces.
  • Should the shared definition also carry the user-facing message text?
    Carry stable machine-readable identifiers for each constraint, and let the client render the wording. Message text needs the user's language, the product's voice, and space the service knows nothing about. Sharing identifiers keeps the two sides talking about the same rule while leaving presentation, including translation and tone, where it belongs.

saying these in an interview costs you the question

  • Says the server can skip validation because the client already checked
  • Assumes every rule can be expressed in a shared schema
  • Ignores that deployed clients hold older copies of the rules
  • Hand-maintains two rule sets and expects them to stay aligned
  • Puts user-facing message wording in the shared definition
  • Treats a client-side pass as a guarantee that submit will succeed