Designing a Public API: The Contract You Cannot Take Back
Versioning, limits, authentication, and errors — decisions that become permanent the moment your first customer builds on them.
The difference between an internal API and a public one is not the code, it is your ability to change it. You edit an internal one on a Tuesday afternoon; a public one becomes a contract the moment a customer builds something they depend on.
Settle versioning before launch, not after
Include a version from day one even if it is only `v1`. Adding one later breaks every existing integration. And define explicitly what counts as a breaking change: adding a field is not, but removing one, changing its type, or narrowing what you accept all are — and your customers will assume a stricter definition than yours.
Paginate by cursor, not offset
Offset pagination looks simpler and fails silently: if a row is inserted between two requests, an item is duplicated or skipped. Use a cursor built on a stable, ordered value. The difference does not show up in tests — it shows up months later as a customer's incomplete data with no explanation.
Errors are part of the interface
- The right status code: 400 for a malformed request, 422 for validation failure, 429 for rate limiting, 500 for your fault.
- A stable string error code the caller can branch on, not an English message that may change.
- A message that says what to do, not only what happened.
- A request id on every response — it is the first thing you will ask for when a customer opens a ticket.
An error message that does not tell the developer what to do next is a deferred support ticket.
Limits are protection, not punishment
Rate limit from day one. Add it later and you break customers who grew used to its absence. Publish the limit in response headers — remaining and reset — and return `Retry-After` with a 429. A developer who can see their limit respects it; one who discovers it by being blocked opens a ticket.
Authentication by use case
- API keys: right for server-to-server integration, provided they are revocable, rotatable, and scoped.
- OAuth: necessary when a third-party app acts on a user's behalf, and overkill when a customer integrates with their own data.
- Never accept a key in a URL — it ends up in logs and browser history.
- Show last-used time per key, because that is what lets a customer delete the old one with confidence.
Documentation is part of the product
An API without a copyable example will not be adopted. Generate documentation from an OpenAPI definition so it cannot drift from actual behaviour, and give every endpoint a real request and response. Hand-written docs become wrong within months, which is worse than having none.
For a standard error shape rather than an invented one, RFC 9457 Problem Details is what serious APIs now adopt. Google's API Design Guide is the most complete reference on naming, pagination and versioning, and the OpenAPI specification is what you generate documentation from so it cannot drift from behaviour.
If you are designing a public interface for your own product, the decisions above will follow you for years — review them with us before the first endpoint ships.
FAQ
Where should the version go?
In the path is simpler and more obvious to developers. A header is cleaner in theory and harder to debug in practice.
REST or GraphQL?
REST for most product APIs: easier to cache, document, and rate limit. GraphQL earns its complexity when read requirements vary widely.
How long do I support an old version?
Announce a fixed policy — a year, say — and hold to it. Vagueness here means you will never retire anything.