Skip to content

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.

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 traceId and 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.
  • 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.

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.

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.

  • 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 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.
  • camelCase JSON. Dates are ISO-8601 with an offset. Money is a decimal string plus a currency code, never a float.
  • 200 read · 201 + Location on create · 204 on a command with nothing to return · 409 on a domain conflict · 422 on validation.

See also: Examples · .NET Backend · Angular · Mobile.

👤 Owner: Firas Darwish🗓 Last reviewed: 2026-07-25

اقرأ هذه الصفحة بالعربية