Skip to content
Engineering · 5 min read

Validate at the boundary and trust nothing that crosses it

The claim Most application bugs and a large share of security holes come from the same root cause: data that entered the system in a shape the code did not expect, and was trusted...

A Written by Administrator
Validate at the boundary and trust nothing that crosses it

The claim

Most application bugs and a large share of security holes come from the same root cause: data that entered the system in a shape the code did not expect, and was trusted anyway because nothing checked it at the point it arrived. The fix is a discipline, not a library — validate every input at the boundary where it enters your system, reject what does not conform before it touches any logic, and treat everything that has passed the boundary as known-good so the interior never has to guess.

What "the boundary" means

The boundary is every point where data from outside your control enters your code: an HTTP request body, a query parameter, an uploaded file, a message from a queue, a webhook from a third party, a row read back from an integration you do not own. Anything that originated outside your program is untrusted at the moment it arrives, no matter how friendly the source seems — a webhook from a trusted payment provider can still be malformed, replayed, or forged, and a value your own mobile app sent can still have been tampered with in transit by someone using your API directly.

The mistake is validating in the middle instead of at the edge: a check buried three function calls deep, after the bad data has already been passed around, logged, and used to make a decision. By then the damage is contextual and hard to trace. Validation belongs at the front door, before the data is allowed anywhere.

Parse, don't just check

The strongest form of boundary validation does not merely check that input is acceptable and then pass the original loose value onward — it transforms the input into a precise internal type that cannot represent the invalid states. You do not check that a string looks like an email and then keep passing the string; you parse it into an Email value, and everything downstream receives that type, which by its existence guarantees validity.

// at the boundary: parse into a precise shape, reject on failure
const parsed = OrderSchema.safeParse(request.body);
if (!parsed.success) {
  return respond(422, { errors: parsed.error.issues });
}
// past this line, `parsed.data` is known-good and correctly typed.
// nothing downstream re-checks it, because it cannot be wrong.
const order = parsed.data;

The benefit is that validation happens exactly once, in one place, and the entire interior of your application is freed from defensive checking. A function deep in the system that receives an Order object does not need to ask whether the total is negative or the email is malformed, because those states were made unrepresentable at the boundary. This is what makes the discipline scale: the checks are concentrated, not scattered.

Reject early, reject specifically

When input fails validation, the response should be immediate and precise. Return a client-error status — 422 for a well-formed request with invalid content, 400 for a malformed one — and name what was wrong, per field, so the caller can fix it. A validation failure that returns a generic 500 is a lie: it tells the caller your server broke when in fact their input did, and it sends your team investigating a phantom server fault. The rule is that bad input is the client's error and must be reported as such, clearly enough that they can correct it without guessing.

Validate the values, not just the shape

Structural validation — the right fields, the right types — is necessary but not sufficient. The boundary must also enforce the business constraints that make a value sensible: a quantity that is positive, a date that is not in the past for a booking, a discount that does not exceed the total, a country code that is one you actually ship to. These are the checks that catch the input which is technically well-formed but semantically impossible, and they belong at the boundary alongside the structural checks, because an interior that trusts the boundary is trusting it for meaning as well as for shape.

quantity:  integer, >= 1, <= 999
ship_to:   must be in the set of countries we serve
total:     must equal the sum of line items (recompute, never trust)

That last line is the one teams most often skip: never trust a total, a price, or any value the client could have manipulated to their benefit. Recompute money server-side from authoritative data, and treat the client's version as a claim to verify, not a fact to use.

The security dimension

Boundary validation is also the foundation under most injection defences. Data that is validated and parsed into a typed value at the boundary, then handled through parameterised queries and context-aware output encoding, does not become a SQL injection or a cross-site scripting hole, because it is never concatenated as raw text into a place where it could be interpreted as code. The discipline that keeps your logic correct is the same discipline that keeps it safe: untrusted input is contained and transformed at the edge, and the interior only ever handles values whose shape and meaning are already guaranteed.

The rule in one line

Every piece of data that crosses into your system gets validated and parsed at the point it arrives, is rejected clearly if it does not conform, and is thereafter trusted completely because it has earned it. An application built this way has one place to look when input is wrong, an interior that never second-guesses itself, and a whole category of bugs and vulnerabilities that simply cannot occur because the invalid states were refused at the door.

#security #validation #api design #correctness

Keep reading