Most API breakage is not a technical accident — it is a governance failure. A field gets renamed on a Friday, a status code drifts from 422 to 400, and three integrators wake up to red dashboards. The fix is boring on purpose: version explicitly, make retries safe, and test the contract in CI.
1. Version like you mean it
URL versioning (/v1/charges) wins for public APIs because it is visible in logs, caches, and docs. Never remove a version without a sunset header, a changelog entry, and a migration window measured in months. Stripe's discipline here is the benchmark: old versions keep working while new ones ship.
- Additive changes only inside a major version — new optional fields are fine, renames are not
- Return Sunset + Deprecation headers at least 180 days before removal
- Keep a per-version changelog with before/after payloads, not prose
- Gate breaking changes behind preview headers before promoting them
2. Make every mutation retry-safe
Networks fail mid-request. Without idempotency keys, a retried POST becomes a double charge. Require Idempotency-Key on all mutations, store the keyed response for 24–72 hours, and return the original response on replay with a replayed: true signal for observability.
POST /v1/charges HTTP/1.1
Idempotency-Key: 7f3a-9c2e-41bd
Content-Type: application/json
{
"amount": 4200,
"currency": "usd",
"customer": "cus_9H2K"
}3. Test the contract, not just the code
Unit tests prove your handler works. Contract tests prove your promise holds. Publish an OpenAPI or GraphQL schema from code (never hand-edit the artifact), then run consumer-driven pact tests and breaking-change detectors in CI. If CI is green but the diff renames a field, the pipeline should fail.
“A stable API is a product feature. Every silent breaking change is a churn event you will never see in analytics. — APHYP API review checklist”
A checklist you can steal
- OAuth scopes per resource, key rotation runbook, per-tenant rate limits with RateLimit headers
- Problem+JSON errors with type, title, detail, and traceable instance IDs
- Cursor pagination with opaque cursors — never OFFSET on growing tables
- Postman collection + reference docs generated on every merge to main
Questions, answered
Should I version with URLs or headers?
URL versions (/v1, /v2) are explicit and easy to route, cache, and document. Reserve header versioning for internal platforms where gateway control is absolute. Either way, publish a deprecation policy with dates, not vibes.
When does GraphQL beat REST?
When clients need shaped, aggregated reads across domains and your team can fund schema governance, persisted queries, and per-field observability. Otherwise well-designed REST with sparse fieldsets ships faster.