The Great REST Mythology
I’ve reviewed hundreds of APIs claiming to be RESTful, and maybe five actually were. The rest were just HTTP APIs with delusions of grandeur. This isn’t necessarily a problem, but it shows a basic disconnect between what developers think REST means and what Roy Fielding actually described in his dissertation. Most teams would be better off admitting they’re building RPC-style APIs and optimizing for that reality instead of cargo-culting REST principles they don’t understand.
True REST requires stateless interactions, uniform interfaces, and hypermedia as the engine of application state (HATEOAS). When’s the last time you saw an API response that included links to related resources? When’s the last time your client discovered endpoints dynamically instead of hardcoding them? Exactly. You’re building HTTP APIs, and that’s perfectly fine. Let’s design them well.
Resource Modeling: The Art of Saying No
The biggest mistake I see in API design is trying to expose every database table as a resource. Your internal data model is not your API. Period. I once inherited an API that had 47 endpoints because someone thought every join table deserved its own resource. The cognitive load was unbearable, and clients ended up making 12 requests to display a simple user profile.
Start with the operations your clients actually need, then work backwards to resources. If you’re building an e-commerce API, your clients care about “get products with inventory and pricing” not “get product, then get inventory, then get pricing rules, then calculate final price.” Design resources that match client workflows, not your database schema. The `/products/{id}/full` endpoint that returns everything in one shot will be more popular than your perfectly normalized individual resources.
Aggregation is not cheating. Composition is not impure. Your clients don’t care about your database normalization philosophy when they’re trying to render a page in under 200ms. Give them what they need in the fewest round trips possible.
Pagination: Where Good Intentions Go to Die
Offset-based pagination with `limit` and `skip` parameters feels intuitive until you realize it breaks horribly under real-world conditions. Page 50 of a constantly changing dataset returns different results every time someone creates or deletes a record. I’ve debugged applications where users reported seeing the same item twice or missing items entirely because of this basic flaw.
Cursor-based pagination is mathematically sound but requires more upfront design. Instead of `GET /items?limit=20&offset=100`, you use `GET /items?limit=20&after=eyJpZCI6MTIzfQ`. The cursor is typically an encoded reference to the last item from the previous page. GitHub’s API does this well: each response includes `next` and `prev` URLs with opaque cursors embedded.
The key insight is making cursors opaque to clients. Don’t expose implementation details like timestamp sorting or composite keys. Encode everything the server needs to continue iteration and let clients treat cursors as magic strings. This future-proofs your pagination when you inevitably need to change the sorting logic or add secondary sort keys for tie-breaking.
Error Handling: Beyond the Happy Path
Most APIs handle the happy path reasonably well and completely fall apart when things go wrong. HTTP status codes are not enough. A `400 Bad Request` tells me something is wrong but not what or how to fix it. A `500 Internal Server Error` tells me even less. Your clients need structured error responses that don’t require psychic debugging skills.
I prefer the RFC 7807 Problem Details format: a standardized structure with `type`, `title`, `detail`, and optional `instance` fields. Instead of returning `{“error”: “Invalid input”}`, return `{“type”: “/errors/validation-failed”, “title”: “Validation Failed”, “detail”: “The email field must be a valid email address”, “invalid-params”: [{“name”: “email”, “reason”: “must be valid email format”}]}`. This gives clients enough information to display meaningful error messages and programmatically handle different error types.
Rate limiting errors deserve special attention. Don’t just return `429 Too Many Requests`. Include `Retry-After` headers and explain the rate limit structure. Stripe’s API returns detailed rate limit information in response headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. Clients can implement intelligent backoff strategies instead of hammering your servers.
Versioning: The Inevitable Compromise
API versioning is where idealism meets reality and gets thoroughly beaten up. URI versioning (`/v1/users`, `/v2/users`) is supposedly ugly and violates REST principles, but it works. Header-based versioning (`Accept: application/vnd.api+json;version=2`) is more “correct” but makes testing and debugging painful. Media type versioning is elegant in theory and a nightmare in practice.
After maintaining APIs through multiple major versions, I’ve settled on URI versioning with a twist: version the entire API surface, not individual resources. `/v1/` and `/v2/` are different APIs that happen to share infrastructure. This makes it crystal clear which version a client is using and eliminates the complexity of per-resource versioning matrices.
The real secret is designing for evolution from day one. Use extensible formats like JSON with optional fields. Add new fields freely (they’re ignored by older clients) but never remove or rename existing fields within a major version. When you must make breaking changes, increment the major version and maintain both versions simultaneously until client migration is complete. Shopify ran API v1 and v2 in parallel for over two years during their migration. Plan for this reality.
The Uncomfortable Truth About Consistency
Perfect consistency is the enemy of good APIs. I’ve seen teams spend months debating whether timestamps should be ISO 8601 strings or Unix epochs while their API remained broken in more important ways. Pick a standard, document it clearly, and move on to problems that actually matter to your users.
The best API designs emerge from understanding your specific constraints and user needs, not from following abstract principles. Twitter’s API evolved from a simple REST-ish interface to GraphQL because their clients needed more flexibility. Slack’s API is thoroughly un-RESTful but perfectly suited to real-time messaging workflows. Your API should solve your problems, not win architecture purity contests.
What patterns have you found most valuable in your API design work? Which “best practices” turned out to be worst practices in your specific context?