API Design
The HTTP contract is the one artefact four stacks touch — the backend produces it, Angular, Android and iOS consume it. A convention that drifts here multiplies by four, and two of those clients cannot be redeployed alongside the server.
flowchart LR Api[".NET API<br/>produces the contract"] --> Spec["OpenAPI document<br/>committed, diffed in CI"] Spec --> Web["Angular"] Spec --> And["Android"] Spec --> Ios["iOS"]
The Backend Lead owns this contract; the client stacks consume it and do not change it unilaterally — see Who owns what.
One error body
Section titled “One error body”Every non-2xx response is RFC 9457 application/problem+json. Validation failures, domain conflicts, and unhandled errors all use the same shape, so a client writes one error path instead of three.
- A 500 body carries a
traceIdand a fixed title. Never an exception message, and never an inner-exception chain — that is an information leak and an unstable contract at the same time. - Full detail goes to the log, correlated by that same id.
- Never parse an error string for control flow. If a client needs to branch on a failure, the failure needs a stable machine-readable field.
Versioning
Section titled “Versioning”- URL versioning —
/v1/..., with the default version pinned in config. - Inside a version, additive only. A new optional field is fine. A removed field, a renamed property, or a narrowed type is a new version.
- A version stays alive for as long as an installed client needs it. Web clients redeploy with the server; a native app on a user’s phone does not. Retire a version only when the server-enforced minimum client version has moved past it — that gate, not the store, is what makes retirement safe.
Collections
Section titled “Collections”Every collection endpoint takes page and pageSize and returns:
{ "items": [], "page": 1, "pageSize": 50, "total": 0 }Offset paging is the default because our tables show page numbers. Keyset paging is the named escape hatch for large or fast-moving data — decided per endpoint, recorded in the repo.
Idempotency
Section titled “Idempotency”Any non-GET a client may retry takes an Idempotency-Key header. The server stores the key with the result and replays the stored result on a duplicate — it does not reject the retry.
This matters most on mobile: a request that succeeded on the server and timed out on the device is normal, and the user’s fix is to press the button again.
OpenAPI is the contract
Section titled “OpenAPI is the contract”- The document is generated from the endpoints and committed. CI regenerates it and diffs against the committed copy — a removed field, a renamed property, or a narrowed type fails the PR. Additive changes pass.
- Clients generate from that document. A hand-written DTO in Angular, Kotlin, or Swift needs a reason in the PR.
Authorization
Section titled “Authorization”Authorization is server-side, per endpoint, permission-based. An Angular route guard or a hidden mobile button is UX — never protection. Every protected endpoint enforces its own permission, and an integration test proves it.
- Nouns and plural collections:
/v1/branches/{id}/visits. No verbs in paths. camelCaseJSON. Dates are ISO-8601 with an offset. Money is a decimal string plus a currency code, never a float.200read ·201+Locationon create ·204on a command with nothing to return ·409on a domain conflict ·422on validation.
See also: Examples · .NET Backend · Angular · Mobile.