I've broken a lot of APIs. I've also built a few that I later had to apologize for at standup. After years of doing both, I have opinions — strong ones — about what separates an API people want to use from one that becomes the subject of a passive-aggressive Slack message at 11 PM.
This isn't a textbook. It's the stuff I wish someone had handed me before I spent three days debugging a system that returned 200 OK for every error.
Let's get into it.
The One Thing Nobody Talks About: Design for the Consumer, Not the Database
Here's the mistake most backend developers make — and I made it too. You look at your database schema, and you basically mirror it into your API. One table, one endpoint. Feels clean. Feels logical.
It's wrong.
Your API is not a database interface. It's a product. The person calling it doesn't care that you store users and profiles in separate tables. They want a single GET /users/{id} call that gives them everything they need to render a profile page, not a chain of five requests that they have to stitch together client-side.
Design your API around use cases, not data structures. Ask yourself: "What is my consumer actually trying to accomplish?" Start there.
REST Is Not a Religion
REST gets treated like commandments handed down from a mountain. Thou shalt use nouns. Thou shalt use the correct HTTP verb. And look — most of that is good advice. But I've seen teams spend a full sprint arguing about whether a logout action should be DELETE /sessions or POST /auth/logout while actual product work piled up.
Know the principles. Apply them with judgment, not rigidity.
The core ideas that actually matter in practice:
-
Use nouns for resources, not verbs.
/articlesnot/getArticles. - Lean on HTTP methods correctly. GET reads, POST creates, PUT/PATCH updates, DELETE removes. Don't use GET for anything that changes state — ever.
-
Keep your hierarchy shallow.
/users/{id}/postsis fine./users/{id}/posts/{postId}/comments/{commentId}/likesis a cry for help.
If REST feels like it's fighting you, consider whether GraphQL or gRPC fits your problem better. They exist for good reasons.
Versioning: Do It From Day One
The single most painful lesson in API development is learning why you need versioning after you've already shipped without it.
The moment you have an external consumer — even one — you have a contract. Breaking that contract without warning is how you end up in meetings you don't want to be in.
Version your API in the URL: /v1/users, /v2/users. Yes, it's a little ugly. Yes, it's absolutely worth it. Some teams prefer versioning via headers (Accept: application/vnd.myapp.v2+json), and there are good arguments for that too — but URL versioning wins on discoverability and simplicity.
The rule is simple: backward-incompatible changes always get a new version. Adding a new optional field? Fine, non-breaking. Renaming a field? New version. Removing a field? New version, and give people a deprecation window.
Stripe's API versioning strategy is worth reading if you want to see how a team that handles billions of API calls thinks about this.
HTTP Status Codes: Please Use Them Correctly
I cannot stress this enough. The number of APIs I've worked with that return 200 OK with a body of {"success": false, "error": "User not found"} is too high for my blood pressure.
HTTP status codes are not decoration. They are communication. Use them.
Quick reference that actually covers 90% of cases:
| Code | When to use it |
|---|---|
200 OK |
Successful GET, PUT, PATCH |
201 Created |
Successful POST that created a resource |
204 No Content |
Successful DELETE (nothing to return) |
400 Bad Request |
The client sent something malformed |
401 Unauthorized |
Not authenticated |
403 Forbidden |
Authenticated but not allowed |
404 Not Found |
Resource doesn't exist |
409 Conflict |
State conflict (duplicate, etc.) |
422 Unprocessable Entity |
Valid format, but business logic failed |
429 Too Many Requests |
Rate limit hit |
500 Internal Server Error |
You broke something |
The 401 vs 403 distinction trips people up constantly. 401 means "I don't know who you are." 403 means "I know exactly who you are, and you can't do this." Different problems, different responses.
Error Responses Deserve as Much Thought as Success Responses
When something goes wrong, your error response is the most important thing you'll return. A developer hitting your API at midnight, debugging a production issue, is depending on it.
A good error response has:
- The right HTTP status code (see above)
- A machine-readable error code (not just the message)
- A human-readable message
- Enough context to actually debug the problem
Something like this:
json
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request body contains invalid data.",
"details": [
{
"field": "email",
"issue": "Must be a valid email address."
}
],
"request_id": "req_8f3k2j1m"
}
}
That request_id is something junior developers often skip. Don't skip it. When a user files a support ticket, that ID is what lets you find the exact log line within seconds instead of minutes.
Pagination: Pick a Strategy and Commit
Returning 50,000 records in a single response is not an API design. It's a disaster waiting for the right load to trigger it.
There are two main approaches to pagination:
Offset-based: GET /posts?page=2&limit=25
Simple to implement, simple to understand. Works well for most use cases. Falls apart at scale when users are inserting or deleting records between pages (you get duplicates or skipped items).
Cursor-based: GET /posts?cursor=eyJpZCI6MTIzfQ&limit=25
Slightly more complex to build and consume, but stable. No skipping, no duplicates. What Twitter and Slack use for their APIs at scale.
Whatever you pick, be consistent. Don't use offset pagination on /posts and cursor pagination on /comments. Your consumers will hate you.
Always include in your response: the current page/cursor, a link or token to the next page, and ideally the total count (when it's feasible to compute).
Authentication: Don't Invent Your Own
Use OAuth 2.0 for delegated access. Use API keys for server-to-server. Use JWTs carefully — they're powerful but misunderstood enough that they deserve their own article.
What I'd specifically avoid: rolling your own authentication scheme. I've seen this done with the best intentions ("our use case is special"). The outcome is almost always the same: a subtle security hole discovered much later, usually at the worst possible time.
The protocols exist. They've been battle-tested by people whose full-time job is thinking about security. Use them.
One practical note: always transmit tokens in the Authorization header, not in the URL. URLs end up in logs. Logs get shared. You don't want your API keys in a log file that someone's pasting into a Slack channel for debugging.
Rate Limiting: Protect Yourself and Be Transparent About It
Rate limiting is not optional if your API is public or even semi-public. Without it, one misbehaving client — or one developer who wrote an accidental infinite loop — can take down your service for everyone.
When you rate limit, be transparent about it. Return 429 Too Many Requests and include headers that tell the client what's happening:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1715894400
Retry-After: 60
This is the difference between a client that backs off intelligently and retries at the right time, versus one that hammers your endpoint even harder trying to get through. RFC 6585 formalized 429 — it's worth a quick read.
Idempotency: The Concept That Saves You at 3 AM
An idempotent operation is one you can call multiple times and get the same result. GET requests should always be idempotent. DELETE should be too — deleting something that's already deleted should return success (or 404), not an error.
Where this gets interesting is with POST requests. By their nature, POST operations aren't idempotent — calling POST /orders twice creates two orders. But networks are unreliable. Clients retry. What happens when a payment request gets retried because the response timed out, even though the first request succeeded?
The solution is idempotency keys. Accept a client-generated key in a header (Idempotency-Key: abc123), and if you see the same key twice, return the cached result of the first request instead of executing again. Stripe pioneered this pattern and documents it well — it's worth stealing.
Documentation Is Part of the API
An API without good documentation isn't finished. It's a mystery box that other developers have to reverse-engineer.
OpenAPI (Swagger) is the standard for REST API documentation. It gives you machine-readable specs that can auto-generate client libraries, test suites, and interactive documentation. Tools like Swagger UI and Redoc turn those specs into beautiful, browsable docs.
But tooling aside — good documentation needs:
- Working, copy-pasteable code examples in multiple languages
- Clear explanation of authentication
- A full list of possible errors and what they mean
- A changelog so developers know what changed and when
Stripe, Twilio, and GitHub are the gold standard for API documentation. Spend an hour exploring any of them before you write yours.
Naming Conventions: Boring Is Good
This shouldn't need a section, but I've seen enough getUserById, fetch_article, and ArticleList endpoints in the same API to know it does.
Pick a convention and apply it everywhere:
-
snake_case for JSON fields (
user_id, notuserIdorUserId) -
kebab-case for URL paths (
/blog-posts, not/blogPosts) -
Plural nouns for collections (
/users, not/user) -
Consistent date formats — use ISO 8601:
2025-05-12T10:30:00Z, always
Inconsistency forces developers to keep a mental map of your quirks. Consistency lets them make correct guesses, which means they spend less time reading your docs and more time building.
A Note on HATEOAS (And Why Most APIs Skip It)
HATEOAS — Hypermedia as the Engine of Application State — is the idea that your API responses should include links to related actions, so clients can navigate without hardcoding URLs. It's the full REST vision.
In theory, elegant. In practice, almost nobody implements it completely, and most consumers don't use it even when they do. I mention it because you'll encounter the term, and I don't want you going down a three-week rabbit hole when you could be shipping.
Closing Thought
Good API design is mostly about empathy. The person on the other end of your API is a developer with a deadline, probably drinking cold coffee, trying to get something working. Every confusing field name, every wrong status code, every missing error message is a small tax on their time and energy.
Design APIs you'd want to use. Test them by actually using them. Read the error messages as if you've never seen the codebase. The difference between a frustrating API and a delightful one is usually that simple.
Found this useful? Drop a comment or follow for more backend content. I write about the things that actually come up in production, not just the stuff that looks good in tutorials.

Top comments (0)