Service boundaries are where systems lie to each other. Inside a service, the compiler keeps everyone honest; at the boundary, data arrives as bytes with a reputation, and every field is a claim someone else made. TypeScript, for all its virtues, evaporates at runtime — a compile-time type annotation offers exactly zero protection against a request body that simply disagrees. Which is why every boundary in Players Engine is guarded by a schema that exists at runtime, and why each schema is written precisely once.
Three artifacts, one truth
Every API endpoint needs three things that describe the same contract: a static type (so handler code autocompletes and type-checks), a runtime validator (so garbage is rejected at the door), and documentation (so the gateway config, the frontend and future-me know what the endpoint accepts). Write those as three separate artifacts and you've created a race: they agree on day one and drift forever after. The field renamed in the type but not the validator. The constraint documented but never enforced. Each drift is invisible until it's a production incident with a confusing shape.
Derive, don't duplicate
The fix is architectural, not disciplinary: make one artifact the source and derive the rest. Our backend services define schemas with TypeBox, which builds standard JSON Schema objects that the web framework validates against on every request — and from the very same objects, TypeScript types are inferred statically and OpenAPI documentation is generated for the gateway. One definition, three jobs. Renaming a field is one edit, and everything that could disagree about it is regenerated instead of remembered. Discipline you don't need is discipline that can't fail.
Validation is a kindness, not a wall
A schema rejection should read like an error message, not an insult. Because validation happens declaratively at the framework layer, rejected requests get structured responses naming the field and the constraint — which turns a class of frontend debugging sessions ("why is this 400ing?") into a five-second read. The same declarations enforce upper bounds: lengths, ranges, enum membership, array sizes. Bounds checking at the boundary is also a cost-control measure — it's much cheaper to reject a pathological payload at the door than to discover it in a database bill, a theme that runs through this whole architecture.
Responses are a boundary too
Validating input is table stakes; the underrated half is validating what goes out. Every route here declares a response schema, and serialization strips anything the schema doesn't name. That narrowness is a security control, not a formality: internal fields added to a database record later — moderation flags, internal scores, whatever the future brings — don't leak into public API responses by default, because the contract is an allowlist rather than "whatever the object happened to contain". The same thinking applies on writes: update endpoints accept named fields, never a whole object to merge, which is the difference between an API and a mass-assignment vulnerability with documentation. Contracts bound in both directions, or they're not really contracts.
The practical dividend shows up in code review: when both sides of every route are schemas, a reviewer reads the contract diff first and knows the blast radius before reading a line of implementation. Schemas turn "what changed?" into a question with a short, honest answer.
The legacy corner
Honesty section: two of the older services still use a different validation library from an earlier era of the codebase. It works, it's tested, and rewriting it tomorrow would be motion without progress. The rule that keeps the situation sane is containment: the legacy pattern may not propagate. New services and new endpoints use the standard approach, no exceptions; the old corner shrinks by attrition, one natural refactor at a time. Every codebase past its first birthday has a corner like this. The failure isn't having one — it's letting it metastasize because "that's how service X already does it" felt like precedent instead of debt.
Schema-first as a habit
The workflow that fell out of all this: the schema gets written before the handler. Designing the contract first forces the questions that matter early — what's required versus optional, what are the bounds, what does the error surface look like — while they're still design questions rather than migration projects. The handler then gets written into a fully-typed slot with validated inputs, which is the closest thing to rails this profession offers. It's the same boundary paranoia that drives the config validation and the auth chokepoint: assume nothing you didn't verify, and make verification the path of least resistance.