Every API ages. The question is whether it ages like wine or like milk. Six rules we apply on every public surface we ship — internal or external.
1. Names outlive everything
You can change the database, swap the framework, rewrite the backend. You cannot easily change a field name once a customer is reading it. Spend the time. created_at, not cAt.customer_id, not cid. cancelled, not canceled (or pick one and never deviate).
2. Version at the boundary
Put the version in the URL or a header — pick one and commit. Then treat each version as a frozen contract. The instinct to "just add a field" is fine for additive changes. It is poison the first time you need to change a meaning.
3. Pagination on day one
Every list endpoint paginates. Every one. Even the ones where "there will only ever be ten of them." There won't. Cursor-based pagination is the right default — offset breaks under concurrent inserts and gets quadratically slow.
// Cursor pagination, opaque to the client.
GET /v1/invoices?limit=50&cursor=eyJpZCI6MTIzfQ
// Response includes the next cursor (null if done).
{
"data": [...],
"next_cursor": "eyJpZCI6MTczfQ"
}4. Errors are part of the contract
An error response is part of your API surface, not an afterthought. It needs a stable code, a human message, and a structured body. Customers will write switch statements on your error codes. Treat that as a contract.
{
"error": {
"code": "invoice_not_found",
"message": "No invoice with that id.",
"request_id": "req_01HXY..."
}
}5. Idempotency keys
Any non-idempotent endpoint — anything that creates, charges, or sends — needs an Idempotency-Keyheader. Stripe popularised the pattern; it's now the default expectation. The first time a client retries a duplicate charge, you'll be glad you put it in v1.
6. Timestamps everywhere
Every record has created_at and updated_at. ISO 8601, UTC, never local. The day you need to debug a race condition or an audit log, you'll need both. The day you need to answer "when did this status change?" you'll wish for a third one — status_changed_at — so add that too.
None of these rules are clever. They're just the ones that compound. We've seen what happens to APIs that skipped them — it's in most of our migration case studies.
End of article · Thanks for reading