Menu

Validation at API boundaries: client, server and the gap between

Validation at API boundaries belongs on both sides: the client for instant feedback, the server for authority. Confusing the two produces messages that mislead users.

Published

  • validation
  • API
  • test data

A number enters a product at more than one place. It is typed into a form, echoed through a client, transmitted to a service, stored, and later read back by an operator. Every one of those points is a boundary, and each boundary has a different reason to check the value.

Teams that treat all of them as the same check end up with duplicated logic that drifts apart, and with the worst of both worlds: slow feedback because the authoritative check is remote, and weak authority because the fast check is the only one that actually runs. Separating the layers fixes both.

Should validation live on the client or the server?

Both, and for different reasons. The client is where latency matters. A user typing a long identifier wants to know about a mistyped character immediately, and a round trip per keystroke is the wrong way to tell them. Local, offline checks that need no lookup belong here.

The server is where authority matters. Anything the client asserts can be forged, skipped by calling the endpoint directly, or produced by an older build with a stale rule set. The service must re-run the check on anything it accepts, not because it distrusts its own client but because the client is not part of its trust boundary.

The two layers should share one specification and one implementation, published as a package or a generated artefact, so the fast check and the authoritative check cannot disagree about what a valid value looks like. When they do drift, the symptom is an input the client accepts and the server rejects, which users experience as an inexplicable failure at the end of a form.

The examples discussed here are structural. No real account, identity or card number should appear in a request log, a test fixture or an error payload, and the values described in this article are illustrative shapes only.

What to check at the boundary and what to let through

The boundary should confirm what it can confirm cheaply and pass the rest onward as data.

Check at the edge: the character set, the length, the normalised form, and any published check character for a scheme the service knows. Reject early and with a specific reason, because the alternative is a malformed value travelling deeper into a system that has no vocabulary for describing it.

Let through: anything requiring a register lookup, unless the service has a contractual relationship that makes the lookup cheap. An account-existence check at a public endpoint is a denial-of-service vector and a privacy hazard at the same time, since it turns a form into an oracle for guessing whether a value is live.

Normalise once, at the boundary, and store the normalised form as the canonical value while keeping the original for display. Everything downstream then compares one representation, and the class of bugs where the same number appears in three formats disappears.

Classifying error responses and their retry semantics

Not every rejection means the same thing, and a single error code for all of them forces every caller to guess how to respond.

Situation Meaning Retryable
Malformed input The value breaks the format rule No — the caller must send different data
Failed check character The shape is legal but the arithmetic disagrees No — same reason
Unsupported scheme Nothing the service implements matches this shape No — unless the service adds coverage
Temporarily unavailable A dependency the check needs is down or throttled Yes, with backoff
Rate limited The caller has exceeded an allowance Yes, after the interval the response indicates

Collapsing the first four into a generic bad request is the most common design mistake, because it makes a permanent input error indistinguishable from a transient outage. Clients then retry the wrong failures or give up on the ones that would have succeeded.

Return a stable machine-readable code alongside a human-readable message, and document which codes are retryable. Treat that classification as part of the interface contract: changing it later is a breaking change for anyone who wired retry logic to it.

Why must a failed check not be reported as a missing number?

Because the two statements have different truth conditions. A failed check character says the string does not agree with itself, which the service knows with certainty. A missing number says no such account or record exists, which the service usually cannot know at all.

The confusion causes real harm in both directions. Told that a number does not exist, a user with a genuine value may abandon a legitimate transaction or re-enter something else. Told that a number is valid because a check passed, a user may believe an account has been confirmed when only the arithmetic has been.

The right message describes the string: the format matched, or the check digit did not hold, or no implemented rule recognises the input. Nothing in that list asserts anything about the world, and each item tells the caller something they can act on. The same discipline applies inside bulk jobs, where a mislabelled category can send a reviewer hunting for a defect that does not exist; the categorisation used for large files is built on exactly this distinction.

Rate limits, timeouts and fallbacks for external lookups

As soon as a check needs data from outside the process, it acquires the failure modes of a network call. Designing for them is not pessimism; it is the difference between a degradation and an outage.

Every external call needs a timeout shorter than the request that contains it, so that a slow dependency does not consume the whole budget. It needs a retry policy with backoff and jitter for the transient failures, and a circuit breaker so that a persistently failing dependency stops being called at full rate.

It also needs a defined behaviour for the case where the lookup cannot happen. Two answers are legitimate, and the choice is a product decision: fail the request, or accept the value provisionally and mark it as unverified. What is not legitimate is silently treating an unreachable lookup as a pass, because that converts an outage into a data-quality problem.

Caching helps and needs its own rules. Cache only what the contract permits, key on the normalised value, and give entries a lifetime matched to how quickly the underlying fact can change. Never cache a rejection as though it were a checked fact about the number.

For developers: contracts, versions and logs

Treat the rule set as a versioned dependency of the API, not as an implementation detail.

Expose which rule set version produced a verdict, so a caller can tell whether a change in behaviour came from its own build or from the service. Version the rule set independently of the endpoint when the coverage changes, and keep old versions available long enough for clients to migrate. When a scheme publishes new parameters, the change should be a data update with a new version number, not a code edit whose release notes omit the behavioural difference.

Log verdicts, never full values. Record the scheme, the verdict, the rule set version and a correlation identifier, and keep the value out of the log line entirely — including in error paths, which is where leaked values most often appear.

Finally, remember what the boundary cannot establish. A validated format is a statement about a string, as the format-only case makes plain for schemes with no arithmetic at all, and the three-layer model of validation is the reference for keeping the layers separate.

Next steps

Take the endpoint that receives your most sensitive identifier and list every check it performs, marking each one as local arithmetic or external lookup. Then confirm that the client runs the local ones early and that the server runs all of them again; the number validation tool shows the verdict wording a client can safely reuse without claiming more than the arithmetic supports.

Keep reading

Credit Card & SSN Validator guides