
The Real Cost of a Bad API
Most API design advice dances around the obvious. You get lectures about RESTful purity, versioning strategies, and the difference between PUT and PATCH. But after shipping a dozen internal services and a couple of public APIs, I’ve learned something that no style guide captures: the goal isn’t to build a “correct” API. The goal is to build an API that feels obvious.
A developer should be able to read your endpoint name and know exactly what it does. They should call your function with the wrong argument and get an error that points to the exact line in their code, not a generic 500 with a stack trace they need to grep through. When an API is hard to misuse, you spend less time on support calls and writing defensive documentation that nobody reads anyway.
I’m Raj. I’ve spent years in the trenches of system integration and backend design. What I’m about to share isn’t academic. It’s the result of cleaning up messes that could have been avoided if someone had spent twenty extra minutes thinking about the developer who’d be calling their code at 2 AM.
Start With the Caller, Not the Database
Too many APIs are thin wrappers around a database schema. If your users table has thirty columns, the GET /users endpoint returns thirty columns. Every time. This is lazy. The database is your implementation detail. The API is a contract.
Think about what the person on the other end actually needs to accomplish. If they’re building a profile page, they need a username, an avatar URL, and a bio. They don’t need the bcrypt hash of the password or the internal user ID. Expose what matters for the task, not what happens to live in the row.
This approach also makes your API more resilient. When you split that users table into two tables during a database migration, the API contract doesn’t have to change. The caller never knew about the join in the first place.

Name Things Like a Human, Not a Compiler
I’ve seen endpoints named /api/v2/entity/fetchByCorrelationKey. The person who wrote that probably felt clever. Everyone who had to integrate with it probably spent ten minutes reading docs to figure out what a “correlation key” was.
Use words that appear in the user’s domain. If you’re building an e-commerce API, call it /orders, not /purchaseTransactions. If the action is to cancel something, the endpoint should be POST /orders/123/cancel, not POST /orders/statusUpdate with a cryptic body parameter. Consistency matters more than cleverness. Pick a convention and stick to it across every endpoint. Don’t use snake_case in one place and camelCase in another because two different engineers had opinions.
The Verb Test
A good API passes the verb test. You should be able to read the endpoint out loud and immediately know what HTTP method belongs there. GET /users – obvious. POST /users – clear. PUT /users/42 – fine. But if you find yourself staring at POST /users/getActive, you’ve already lost. That’s a GET disguised as a POST, and it means someone was afraid of query parameters or didn’t want to configure their caching layer properly.
Make the Wrong Thing Impossible
This is where most APIs fail. They document what you should do but don’t prevent what you shouldn’t. A classic example: an endpoint that accepts a status field. The docs say “Valid values are ‘active’, ‘pending’, ‘closed’.” But the code accepts any string and passes it straight to the database. Now you have records with a status of ‘Actve’ because someone made a typo at 5 PM on a Friday.
Validate at the boundary. Reject anything that doesn’t match the expected set. Return a 400 with a message like “Invalid status: ‘Actve’. Expected one of: active, pending, closed.” The developer gets immediate feedback, and your data stays clean.
Another pattern: use specific types instead of generic ones. If an endpoint requires a date, don’t accept a string and hope it’s in ISO 8601 format. Accept a proper date type. If your language or framework doesn’t enforce this, write a thin validation layer that does. The extra code is trivial compared to debugging a production issue caused by a date that was interpreted as month-first in one service and day-first in another.
Don’t Trust Defaults
Default values are a trap. You add a page_size parameter with a default of 20. A developer doesn’t read the docs, calls the endpoint without it, and gets 20 results. They assume that’s all the data. Their feature ships with missing records. If you must have a default, make it something that forces awareness. Return an error if page_size isn’t explicitly set, or use a default of 0 that returns nothing. Silent assumptions are the enemy of correct integrations.

Errors That Actually Help
An error response should answer three questions: What went wrong? Why did it go wrong? What do I do next? A generic {"error": "internal_error"} answers none of them. The developer’s only move is to copy-paste the error into Slack and hope someone else knows what it means.
Instead, give them something actionable:
{
"error": "invalid_parameter",
"message": "The 'email' field must be a valid email address.",
"details": {
"field": "email",
"received": "not-an-email",
"expected": "string matching email format"
}
}
This tells the developer exactly which field is wrong, what they sent, and what you expected. They can fix it without opening your docs, without pinging your team, and without swearing at their monitor. Good error messages are a form of respect.
Also, use the right HTTP status codes. A 400 for bad input. A 404 when a resource doesn’t exist. A 409 when there’s a conflict. Don’t return 200 with a body that says “error” because you wanted the request to always succeed at the HTTP level. That’s just lying to the transport layer.
Versioning Without the Drama
Everyone has an opinion on API versioning. URL versioning (/v1/, /v2/) is ugly but it works and it’s dead simple to understand. Header-based versioning is more “pure” but every integration now has to set a custom header, and your docs have to explain it in triplicate. I default to URL versioning because it reduces the cognitive load on the caller. They can see the version right in the URL and test it in a browser without special tools.
The real trick with versioning isn’t the mechanism—it’s knowing when not to version. Adding a new field to a response? That’s backwards-compatible. Don’t bump the version. Adding a new optional query parameter? Compatible. Removing a field or changing the meaning of an existing one? That’s a breaking change and warrants a new version. Breaking changes should be rare and deliberate. If you’re releasing a new API version every quarter, your design process is broken.
Test From the Outside In
Unit tests are fine, but they test your code, not your contract. Write integration tests that make real HTTP calls to your API and verify the responses. Better yet, write a small client in a different language than your server. If your API is in Python, write a quick Node.js script that exercises the main endpoints. You’ll catch serialization quirks, date format mismatches, and assumptions you didn’t know you were making.
I once worked on an API that returned timestamps as Unix epoch integers in development but as ISO 8601 strings in production because of a difference in JSON serialization libraries. An integration test against a staging environment caught it in minutes. The unit tests were all green and completely useless for this bug.
Document What the Code Doesn’t Say
Auto-generated docs from OpenAPI specs are a start, but they’re not enough. They tell you the parameters and the response shape. They don’t tell you that the POST /orders endpoint triggers a payment authorization, or that the GET /users endpoint caches results for 60 seconds, or that rate limiting kicks in at 100 requests per minute per API key.
Write a short section for each endpoint that explains the behavior, the side effects, and the gotchas. Put it next to the auto-generated reference. The auto-gen handles the what; you handle the why and the when-to-use. If you can’t explain an endpoint’s behavior in three sentences, the endpoint is probably doing too much.
And for the love of everything, include real request and response examples, not placeholders. Don’t show {"name": "string"}. Show {"name": "Raj"}. It makes a difference when someone is skimming at speed.
FAQ
Should I always use REST for my API?
No. REST works well for CRUD-heavy services where resources map neatly to entities. If your API is action-heavy—like triggering a report generation or running a batch process—RPC-style endpoints or GraphQL might fit better. The goal is clarity, not adherence to a paradigm. Use what makes the intent obvious to the caller.
How do I handle pagination without confusing developers?
Pick one method and use it everywhere. Cursor-based pagination is more reliable for large datasets but requires a bit more understanding. Offset-based pagination is simpler but breaks down when data changes between requests. Whatever you choose, return metadata in the response: total count, next page URL, previous page URL. Don’t make the caller construct the next URL themselves by splicing query parameters.
What’s the biggest mistake you see in API design?
Designing for the happy path and hoping errors don’t happen. Most API design effort goes into what the response looks like when everything works. The real quality of an API shows in how it handles a missing field, an expired token, or a request that times out halfway through processing. Spend at least as much time on error states as you do on success states.
How many endpoints is too many?
When a single user action requires more than three API calls, reconsider your design. Either you’re exposing too much internal complexity, or you need a composite endpoint that bundles the common workflow. Watch what your frontend developers actually do with your API and optimize for that, not for theoretical completeness.
Wrap Up
An API is a user interface for developers. The same rules of good UI design apply: make the common tasks easy, provide clear feedback, and prevent errors before they happen. Every decision—from naming to error formatting to default values—should be made with a specific developer in mind, not an abstract specification.
The best compliment an API can get isn’t “this is well-designed.” It’s “I integrated with this in an afternoon and didn’t have to read the docs twice.” Aim for that.