The API Design Playbook: Building Interfaces That Last a Decade

The API Design Playbook: Building Interfaces That Last a Decade
I once inherited a codebase where the API endpoint for fetching a user's profile was POST /api/getData. It accepted a JSON body with a field called type that could be "user", "profile", "account", or "userProfile" — all returning slightly different shapes of the same data. There was no documentation. The response format changed depending on whether you passed an id as a string or a number.
This API was two years old and already unmaintainable.
Your API is the most important interface in your software — more important than the UI, because the UI can change without breaking consumers. An API, once released and adopted, becomes a contract. And contracts are painful to amend.
REST Is Not Just URLs and JSON
Let me say something that will annoy some people: most APIs described as "RESTful" aren't. They're JSON-over-HTTP, which is fine, but they miss the principles that make REST actually work at scale.
REST's power comes from its constraints:
Resources, not actions. URLs should identify things, not perform operations. /users/42 is a resource. /getUser?id=42 is an RPC call wearing a URL costume.
HTTP methods carry meaning. GET reads. POST creates. PUT replaces. PATCH updates partially. DELETE removes. If your GET endpoint changes server state, you've broken a fundamental contract that proxies, caches, and browsers rely on.
Status codes communicate intent. 200 means success. 201 means created. 404 means not found. 422 means the data didn't validate. 500 means you screwed up. Don't return 200 with { "error": true, "message": "Not found" }. That's lying to every HTTP client in your stack.
# Good
GET /api/v1/projects → List projects
POST /api/v1/projects → Create project
GET /api/v1/projects/42 → Get project 42
PATCH /api/v1/projects/42 → Update project 42
DELETE /api/v1/projects/42 → Delete project 42
# Bad
POST /api/getProjects
POST /api/createProject
POST /api/updateProject
POST /api/deleteProject
The "bad" pattern isn't technically broken — it works. But it throws away decades of HTTP infrastructure designed around method semantics. Your API gateway, your cache layer, your monitoring tools — they all understand HTTP methods. Use them.
Versioning: The Decision You Can't Defer
You will need to make breaking changes. Accept this now and build for it from day one.
I prefer URL-based versioning (/api/v1/, /api/v2/) over header-based versioning. Yes, URL versioning is technically "less RESTful." But it's explicit, visible in logs, easy to route, and impossible to get wrong. Every developer understands that /v1/ and /v2/ are different things.
The real discipline isn't choosing a versioning scheme — it's defining what counts as a breaking change:
Breaking changes (require new version):
- Removing a field from a response
- Changing a field's type
- Making a previously optional parameter required
- Changing the URL structure
Non-breaking changes (same version):
- Adding a new field to a response
- Adding a new optional parameter
- Adding a new endpoint
- Increasing a rate limit
When you do release a new version, give consumers a migration window. At minimum, 6 months. Run both versions in parallel. Provide migration guides. Deprecation warnings in response headers are a nice touch.
Pagination Is Not Optional
Any endpoint that returns a list will eventually return a list too large to handle in a single response. Build pagination from day one — even if you currently have 12 records.
There are two sensible approaches:
Offset-based (?page=3&limit=20) is simple and works well for UIs where users jump to specific pages. Its weakness: if items are inserted or deleted between requests, you can miss items or see duplicates.
Cursor-based (?cursor=abc123&limit=20) uses an opaque pointer to the last item in each page. It's more robust for real-time data and infinite scrolling. Slightly harder to implement, but worth it for any feed-like interface.
Whatever you choose, always include pagination metadata in the response:
{
"data": [...],
"pagination": {
"total": 247,
"page": 3,
"limit": 20,
"hasMore": true
}
}Error Responses Deserve Design Attention
I judge the quality of an API by how it handles errors. A thoughtful error response tells the consumer exactly what went wrong, where, and ideally how to fix it.
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request body contains invalid fields.",
"details": [
{
"field": "email",
"message": "Must be a valid email address.",
"received": "not-an-email"
},
{
"field": "age",
"message": "Must be a positive integer.",
"received": -5
}
]
}
}Compare that to { "error": "Bad request" }. Both return 400. One is cruel, the other is kind.
Every error response should have:
- A machine-readable code (for programmatic handling)
- A human-readable message (for debugging)
- Relevant context (which field failed, what was expected)
Rate Limiting: Protect Yourself Politely
Every public API needs rate limiting. Not eventually — from launch. A single misbehaving client can bring down your infrastructure.
The standard pattern uses X-RateLimit-* headers:
X-RateLimit-Limit: 100 # max requests per window
X-RateLimit-Remaining: 67 # requests left in current window
X-RateLimit-Reset: 1696118400 # when the window resets (Unix timestamp)
When a client exceeds the limit, return 429 Too Many Requests with a Retry-After header. Don't silently drop requests or return 500. Tell the client what's happening and when they can retry.
Tiered rate limits make sense for APIs with free and paid tiers: 100 requests/minute for free, 1,000 for paid, 10,000 for enterprise.
Authentication: Keep It Simple
JWT for stateless APIs. API keys for server-to-server. OAuth 2.0 when third-party access is required.
That's it. I've seen teams implement custom authentication schemes that were creative, technically impressive, and an absolute nightmare for consumers. Use the boring, well-documented standards.
One non-negotiable: never accept credentials over HTTP. HTTPS only. This seems obvious in 2025, but I still encounter internal APIs served over plain HTTP because "it's behind a VPN." VPNs get misconfigured. TLS is cheap. Use it.
Document Everything (But Write It for Humans)
OpenAPI/Swagger is the standard for API documentation, and it's fine. But auto-generated documentation from your code is only the starting point.
Great API docs include:
- Quick start guide: "Here's how to make your first request in 5 minutes"
- Authentication walkthrough: Step-by-step, with real examples
- Common workflows: "Here's how to create a project and add users to it"
- Error handling guide: "When you see error X, here's what to do"
- Code examples: In at least 3 languages (cURL, JavaScript, Python)
Stripe's API documentation is the gold standard for a reason. Every endpoint has a prose explanation, request/response examples, and language-specific code snippets. It takes effort, but your API is only as good as a consumer's ability to understand it.
The Checklist
Before shipping any API endpoint, run through this:
- URL identifies a resource (noun), not an action (verb)
- HTTP method matches the operation
- Response includes appropriate status code
- Pagination is implemented for list endpoints
- Error responses include code, message, and details
- Rate limiting is in place with proper headers
- Authentication is required and documented
- Versioned in the URL
- Response format is consistent with all other endpoints
- It has at least one integration test
Build APIs like you're building for the developer who inherits your project in three years. Because that developer might be you.