A comprehensive guide to API design-from URL structure to error handling to versioning strategies that age well.
An API is a contract. It says, "I promise this will work this way, and you can build on top of it." Every design choice you make either strengthens that promise or quietly undermines it. Over the years I have built APIs for internal platforms, public products, and partner integrations, and the principles below are what I keep coming back to.
Start with the consumer, not the database
The first mistake I see is APIs that mirror the database schema. The shape of your tables is an implementation detail. The API should reflect what consumers want to do, not how you happen to store data.
Before designing an endpoint, write the call you wish you could make as a consumer. Then work backwards. If the call needs three round trips and a join on the client side, your API is wrong, not the consumer.
Resources, not RPC (mostly)
REST gets dismissed as old fashioned, but the resource model is durable because it forces clarity. A resource has an identity, a state, and transitions. When you can name resources cleanly, the rest of the design tends to follow.
There are exceptions. Search, complex workflows, and bulk operations often map poorly to pure REST. Do not contort the design to fit. A POST to /operations/recompute-billing is fine when it actually describes what is happening. Pragmatism beats purity.
Predictable URLs and verbs
- Plural nouns for collections (/invoices, not /invoice).
- Stable resource IDs in the path, not query strings.
- Filtering, sorting, and pagination as query parameters.
- HTTP methods that match semantics. GET is safe and idempotent. PUT is idempotent. POST creates or triggers. DELETE removes.
If a consumer can guess your next endpoint after reading three of them, you have done good work.
Errors are part of the API
Most APIs handle the happy path well and treat errors as an afterthought. That is backwards. Consumers spend more time debugging errors than celebrating successes.
A good error response includes:
- A stable machine readable code, not just an HTTP status.
- A human readable message that names the actual problem.
- A field path when the error is about a specific input.
- A correlation ID so your support team can trace it.
Use HTTP status codes correctly. 4xx is the caller's fault, 5xx is yours. Returning 200 with an error body is a sin that will haunt your integrators.
Idempotency is not optional
Networks fail. Retries happen. If a consumer can call POST /payments twice and end up with two charges, you have shipped a bug, not an API. Accept an Idempotency-Key header on any state changing endpoint, store the result for a reasonable window, and replay it on duplicate requests. Stripe popularized this pattern for good reason.
Versioning that ages well
I have shipped APIs with URL versioning (/v1/...), header versioning, and date based versioning. They all work. What matters is committing to a strategy and being honest about deprecation.
A few rules I follow:
- Additive changes do not require a new version. New fields, new endpoints, new optional parameters are safe.
- Breaking changes need a version bump and a deprecation timeline measured in quarters, not weeks.
- Communicate deprecations in the response itself, with Sunset and Deprecation headers, not just a blog post that nobody reads.
Pagination, filtering, and limits
Cursor based pagination beats offset pagination for almost every real workload. Offsets get inconsistent under writes and slow down on large tables. Cursors are stable and fast.
Set sensible defaults. A consumer who forgets to paginate should get the first page, not a 50 megabyte response. Document maximum page sizes and enforce them.
Security and rate limits
Authentication should be obvious and consistent across endpoints. If half your endpoints take a bearer token and half take an API key in a query string, you have failed. Pick one approach and apply it everywhere.
Rate limits should be communicated through headers (X-RateLimit-Remaining, X-RateLimit-Reset) so well behaved clients can self regulate. Earlier in my career working on regulated systems I learned that observability of limits matters as much as the limits themselves.
Documentation is the API
If your API is not documented, it does not exist. The good news is that OpenAPI and similar specs let you generate documentation, SDKs, and tests from a single source of truth.
Invest in:
- A reference that is generated from the spec, so it cannot drift.
- A getting started guide that takes a developer from zero to first successful call in under five minutes.
- Recipes for common workflows, written in real code.
For more on how I approach this, see my notes on API and platform engineering and how it ties into broader product surfaces.
A short checklist before you ship
- Can a new developer make their first successful call in under five minutes?
- Are errors actionable and consistent?
- Is every state changing endpoint idempotent?
- Is pagination cursor based with sensible defaults?
- Is there a deprecation policy you are willing to defend?
If any answer is no, the API is not ready. APIs you ship today will outlive most of the code around them. Treat them with the seriousness that deserves. If you want a second pair of eyes on a design, reach out.
References
Tagged
Sri Vardhan
Independent technology studio of one. I help founders and small teams ship serious software without the consultancy overhead. More about me.