Lesson 6 of 9 · 18m
APIs and Versioning at Scale
The interface is the contract — idempotent endpoints, pagination, backwards compatibility, and the versioning strategies that don't break clients.
An API is a promise: every client that integrates with it depends on its shape. The design choices that matter most are the ones that determine whether you can evolve the API without breaking every consumer.
Idempotency is the contract's backbone
POST /payments retried by a client that timed out creates two charges — the fix is an idempotency key (Idempotency-Key: uuid) that the server stores: a retry with the same key returns the first result, not a second charge. Every non-idempotent endpoint needs one; Stripe and every serious payments API proves this pattern works.
Versioning strategies
- URL versioning (
/v1/users,/v2/users) — explicit, cache-friendly, easy to reason about. The downside: two versions forever. - Header versioning (
Accept: application/vnd.api+json; version=2) — keeps URLs clean; invisible in logs and harder to debug. - Schema evolution — additive-only changes (new optional fields, never rename/remove). When it works, it's the least painful; when a breaking change is needed, you still need versioning.
Pagination and rate limiting
GET /items?limit=50&cursor=abc — cursor-based pagination scales with data size; offset=5000 doesn't. Rate limits need honest responses: 429 Too Many Requests + Retry-After + a X-RateLimit-* header family so clients can back off intelligently instead of hammering.
Field notes
- Document the contract, not the implementation — OpenAPI specs that generate docs and test fixtures keep both honest.
- "Deprecation" means announcing sunset, logging usage of the old version, and a real deadline — not just "stop using it."
- Webhooks are APIs in reverse — they need retries, signatures, and idempotent receivers on the consumer side.