Under the Hood: The API as a Long-Lived Network Contract
An API contract is not merely a collection of JSON payloads and HTTP routes; it is a long-lived interface boundary between independent deployment lifecycles. It can evolve through additive, backward-compatible changes, but existing behaviour must not break. In distributed systems, breaking changes deployed to an API immediately cascade into client crashes, silent data corruption, or catastrophic transaction drops.
The Life of an API Contract Across Lifecycles
Core Elements of Production-Grade API Contracts
1. Idempotency Key Semantics
Network timeouts leave callers in an ambiguous state: did the server crash before executing the mutation, or was the response lost in flight? Non-idempotent mutating endpoints (typically POST, and PATCH when it is not naturally idempotent) should accept an Idempotency-Key header, an IETF draft (draft-ietf-httpapi-idempotency-key-header) popularised by Stripe. The server records the key alongside the HTTP response within a single atomic database transaction. Retried requests with the identical key and a matching request body bypass business logic and replay the cached HTTP response; reusing a key with a different body is rejected as an error.
2. Backward Compatibility & Field Deprecation
Never rename or change the type of an existing field. Follow the robustness principle (Postel's law): producers should be conservative in what they send, and consumers liberal in what they accept. The consumer side of this is the Tolerant Reader pattern: ignore unknown fields and read only what you need. New fields must be optional. Deprecated fields must remain populated through formal sunset timelines communicated via Sunset: <date> (RFC 8594) headers.
3. Standardized Error Handling (RFC 9457 Problem Details)
APIs should return uniform machine-readable error responses containing a type URI, human-readable title, HTTP status, granular detail, and a correlation instance trace ID to eliminate ambiguous ad-hoc client parsing.
Worked example: evolving a create-link endpoint
Version 1 of the shortener API is POST /v1/links with body {"url": "https://example.com/a"} returning 201 Created and {"code": "Ab3xY9", "url": "..."}.
Safe change: add an optional expires_at request field and a new created_at response field. Old clients ignore created_at (if they are tolerant readers) and never send expires_at, so their behaviour is unchanged.
Breaking changes: renaming url to destination, making expires_at required, changing the code length that clients validate with a regex, or returning 200 instead of 201. Each needs either a new version or a migration period where both forms are accepted.
Retry safety: the client sends Idempotency-Key: 5f2c.... The server stores the key, a hash of the request body and the final response in the same transaction as the link insert. A retry with the same key and body returns the stored response; the same key with a different body returns an error (for example 422) so accidental key reuse is detected. Keys expire after a retention window, for example 24 hours at Stripe.
Compatibility rules at a glance
| Change | Usually safe? | Notes |
|---|---|---|
| Add optional request field | Yes | Server must supply a sensible default |
| Add response field | Yes, for tolerant clients | Strict deserializers that reject unknown fields will break |
| Add new enum value | Risky | Clients with exhaustive switch statements may crash; document that enums can grow |
| Remove or rename field | No | Deprecate, keep populating, announce with a Sunset header, then remove in a new version |
| Make optional field required | No | Old clients never send it |
| Change field type or units | No | Add a new field instead |
| Tighten validation | No | Previously accepted requests start failing |
Standard building blocks
- Errors: RFC 9457 Problem Details (which obsoletes RFC 7807) defines a JSON error body with
type,title,status,detailandinstance. - Deprecation: the
Sunsetheader (RFC 8594) announces when a resource will stop working; theDeprecationheader (RFC 9745) signals that it is deprecated now. - Idempotency: the
Idempotency-Keyheader is an IETF draft, popularised by Stripe; many providers implement it. - Versioning: URL versions (
/v1) are simple and visible; Stripe uses dated versions pinned per account with a request header to override, translating responses between versions internally.
Failure modes
- Idempotency record written outside the business transaction: a crash between the two lets a retry perform the action twice.
- Concurrent retries with the same key both running: lock on the key or insert it first with an in-progress status.
- Deprecation announced but never measured: log which clients still call old fields before removing them.