API Contract Evolution
Change APIs without breaking clients you can't force-upgrade. design-api covers writing new endpoints; this covers changing them over time.
Versioning Strategy — Choose One
| Strategy | Use when |
|---|---|
URL path (/v1/, /v2/) |
Public APIs, mobile clients — visible, cacheable, easy to test. Preferred. |
Header (API-Version: 2024-01-01) |
Internal service-to-service where all callers are under your control. |
Query param (?version=2) |
Avoid — breaks caching and is easy to forget. |
| GraphQL field evolution | Add fields, @deprecated, remove only after confirmed zero usage. |
Breaking vs Non-Breaking
Non-breaking (safe without a version bump)
- Add optional request fields with sensible defaults.
- Add response fields (test that clients actually ignore unknown fields).
- Relax validation; add new endpoints or enum values.
Breaking (require a new version or deprecation window)
- Remove, rename, or change a field's type or semantics.
- Tighten validation; change response structure or HTTP method semantics.
- Remove an endpoint.
Deprecation Lifecycle
- Add
Deprecation: trueandSunset: <RFC 7231 date>response headers. - Log usage per client (API key, user agent, IP).
- Notify consumers with usage evidence before sunset.
- Return
410 Goneafter sunset — keep the route registered so clients get a clear error. - Remove code after monitored traffic reaches zero.
Minimum window: 3 months for external APIs; 6 months for mobile (app store cycles delay adoption).
OpenAPI as Contract Source
- One OpenAPI spec per version; generate docs, SDKs, and mocks from it.
- CI:
oasdifforopenapi-diffto detect breaking changes. Fail the build on unversioned breaks. - Consumer-driven contract tests (Pact): consumer publishes a pact; provider CI verifies on every change.
SDK Generation
Generate typed SDKs with openapi-generator, fern, or speakeasy. Clients pin the SDK, not raw HTTP. Never auto-publish a major SDK version without a migration guide.
Backward-Compatibility Testing
Record golden request/response pairs from production. Replay against the new version in CI; assert responses match the schema the old client expects. Tools: VCR (Ruby), pytest-recording (Python), Polly.js, MSW record mode.
Guardrails
- Never change a field's meaning without changing its name — semantic drift is invisible to type-checkers.
- Never deprecate without tracking real usage first — unknown callers break silently.
- Never set a sunset date shorter than your longest release cycle.
- Never rely on clients ignoring unknown fields without a test confirming they actually do.