The 2 AM Test
Picture this. It's 2 AM. You're running on cold coffee and desperation. A production system is down, and you need to integrate with an API you've never touched before. The documentation is sparse. The error messages are cryptic. The endpoint naming looks like someone threw darts at a thesaurus. You want to scream.
Now flip that around. What if that API was yours? What if you designed it? Would you be cursing your own name at 2 AM, or would you breeze through the integration because everything just makes sense?
I've been on both sides of this nightmare, and I can tell you — the difference between a good API and a terrible one isn't cleverness or advanced technology. It's empathy. It's the willingness to sit in the chair of the person consuming your API and ask, "Would this make me angry at 2 AM?" That single question, applied relentlessly, will transform the way you design APIs.
Naming Things Like a Human Being
The old joke in computer science is that there are only two hard problems: cache invalidation and naming things. With APIs, naming things isn't just hard — it's everything. Your endpoint names are the first thing a developer sees. They're the storefront of your service. If they're confusing, people will assume the rest is confusing too. And they'll usually be right.
Use nouns for resources and let HTTP methods (GET, POST, PUT, DELETE — the verbs that tell the server what action to take) do the heavy lifting. Instead of POST /createUser or GET /fetchAllOrders, go with POST /users and GET /orders. This follows REST conventions that most developers already have burned into muscle memory. When someone sees GET /users/42/orders, they instantly know: "Get me the orders for user 42." No guessing. No documentation required for the basics.
I've found that a great litmus test is to read your endpoint out loud. If it sounds like a sentence fragment that makes sense — "get users," "delete order 42," "update the product" — you're on the right track. If it sounds like robot-speak — "post user-creation-action-handler" — go back to the drawing board. And please, please be consistent. If you use plural nouns in one place (/users), don't suddenly switch to singular somewhere else (/product). Inconsistency is the silent killer of developer experience.
Errors That Actually Help
Nothing destroys trust faster than an API that returns 500 Internal Server Error with no additional context. Or worse, one that returns 200 OK with an error buried somewhere in the response body. I've seen both. I've debugged both at unreasonable hours. Neither is fun.
Good error responses are a gift to the developer. They should tell you three things: what went wrong, where it went wrong, and ideally, how to fix it. Use proper HTTP status codes — 400 for bad requests, 401 for authentication problems, 404 when something doesn't exist, 422 when the data is technically well-formed but semantically wrong (like trying to set a user's age to negative 5). These codes aren't just bureaucratic overhead. They let developers write proper error-handling logic without parsing your custom message strings.
Here's what a helpful error response looks like versus a useless one:
// Useless
{
"error": true,
"message": "Something went wrong"
}
// Actually helpful
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The 'email' field must be a valid email address.",
"field": "email",
"documentation_url": "https://api.example.com/docs/errors#validation-failed"
}
}That second response tells the developer exactly which field failed, why it failed, and where to learn more. At 2 AM, that's the difference between a five-minute fix and an hour of trial and error. In my experience, teams that invest in rich error messages get dramatically fewer support tickets. It's one of those rare things that saves everyone time — both you and the people using your API.
Consistency Is the Whole Game
Think of your favorite restaurant. You go back because you know what to expect. The quality is consistent. The menu makes sense. Now imagine if every time you went, the menu layout changed, the naming conventions shifted, and sometimes the prices were in dollars and sometimes in euros for no apparent reason. You'd stop going.
APIs work the same way. Once a developer learns one part of your API, they should be able to predict how the rest of it works. If your users endpoint returns a list wrapped in a "data" key, your orders endpoint should too. If you use snake_case for field names on one resource, don't switch to camelCase on another. Pick a date format (ISO 8601, which looks like 2024-01-15T09:30:00Z, is the standard — just use it) and stick with it everywhere.
This extends to how you handle things like pagination, filtering, and sorting. Define a pattern once and reuse it across every list endpoint. If GET /users?page=2&per_page=20 works for users, then GET /orders?page=2&per_page=20 should work identically for orders. Developers build mental models of your API. Every inconsistency forces them to throw out their model and start over. That's not just annoying — it's expensive in terms of their time and your support burden.
Make the Simple Things Simple
Here's a principle I come back to constantly: common tasks should require minimal effort. If 80% of your API consumers need to do the same five things, those five things should be dead simple. You can still support complex use cases, but don't force everyone to deal with that complexity upfront.
Let's say you have an API for sending notifications. Most people just want to send a basic message to a user. Don't make them specify fifteen required fields when sensible defaults would cover most cases. Here's what I mean:
# Overly complex for a simple task
import requests
response = requests.post("https://api.example.com/v1/notifications", json={
"recipient": {"type": "user", "id": "user_42", "channel_preference": "default"},
"message": {"type": "plain", "body": "Your order shipped!", "encoding": "utf-8"},
"priority": "normal",
"delivery": {"strategy": "single", "retry_policy": "standard"},
"tracking": {"enabled": True, "callback_url": None},
"metadata": {}
})
# Simple by default, powerful when needed
response = requests.post("https://api.example.com/v1/notifications", json={
"user_id": "user_42",
"message": "Your order shipped!"
})Both could ultimately support the same full range of options. But the second version lets someone get started in seconds. The advanced fields are there if you need them — they're just optional, with smart defaults filled in server-side. This is sometimes called "progressive disclosure" — reveal complexity only when someone asks for it. Think of it like a car dashboard. You see the speedometer and fuel gauge immediately. The engine diagnostics are there, but you have to go looking for them.
Versioning: Protecting the People Who Trust You
Breaking changes are the nuclear option of API design. Someone built their business on your API. They integrated it, tested it, deployed it, and moved on to other problems. If you pull the rug out by changing a field name or removing an endpoint, you've just created a crisis for them — possibly at 2 AM.
Version your API from day one. The simplest approach is putting the version in the URL path: /v1/users, /v2/users. Some people prefer header-based versioning, which is technically more "pure," but URL-based versioning is visible, obvious, and easy to debug. I'll take practical over pure any day of the week.
When you do need to make breaking changes, give people a migration path. Announce the deprecation well in advance. Run both versions simultaneously for a generous transition period. Provide a migration guide that doesn't assume everyone has been following your changelog religiously. The APIs I've enjoyed working with most are the ones that treated backward compatibility as a sacred promise, not a suggestion.
Documentation as a First-Class Feature
I know, I know. Nobody wants to write documentation. But here's the thing — your API without documentation is like a product without packaging. People won't even try it if they can't figure out how to start.
Great API documentation has three layers. First, a quickstart guide that gets someone from zero to a working request in under five minutes. Second, comprehensive reference docs for every endpoint, including every parameter, every possible response, and every error code. Third, guides and tutorials for common workflows that span multiple endpoints. Most APIs nail the reference docs (especially if they auto-generate from OpenAPI specs) but completely neglect the quickstart and the guides. That's a mistake. Reference docs are where people go when they know what they're looking for. Guides are where people go when they don't.
Include real, working code examples in multiple languages. Not pseudo-code. Not shortened snippets that leave out the imports and setup. Actual copy-paste-and-run examples. Here's a quickstart that respects the developer's time:
// Install: npm install node-fetch
// Get your API key at https://example.com/dashboard
const API_KEY = "your_api_key_here";
const response = await fetch("https://api.example.com/v1/users", {
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
}
});
const data = await response.json();
console.log(`Found ${data.users.length} users`);
// Output: Found 42 usersNotice it includes where to install the dependency, where to get the API key, and what the output looks like. Zero ambiguity. That's what 2 AM-friendly documentation looks like.
Rate Limiting With Respect
Every API needs rate limiting — a mechanism that restricts how many requests a client can make in a given time window. Without it, one runaway script could take down your service for everyone. But how you implement rate limiting says a lot about how much you respect your developers.
Always communicate limits clearly in your response headers. The standard approach is to include headers like X-RateLimit-Limit (the maximum number of requests allowed), X-RateLimit-Remaining (how many they have left), and X-RateLimit-Reset (when the window resets). When someone does hit the limit, return a 429 Too Many Requests status with a response body that tells them when they can retry. Don't just silently drop requests or return a generic error.
I've worked with APIs that punished you for hitting rate limits — long cooldown periods, opaque error messages, zero warning. And I've worked with APIs that treated rate limiting as a collaborative tool. They'd warn you in the headers as you approached the limit. They'd include a Retry-After header telling you exactly how many seconds to wait. Some even offered a way to request higher limits through a simple form. Guess which ones I recommended to other developers?
Think in Developer Journeys
It's easy to design an API endpoint by endpoint, thinking about each one in isolation. But developers don't use your API in isolation. They use it in sequences. They create a user, then add an address, then place an order, then check the order status. Your API should feel smooth across that entire journey, not just at individual stops.
Map out the three or four most common workflows your API supports. Then walk through them yourself, from scratch, as if you've never seen your API before. Time yourself. Note every moment of friction — every place where you had to look something up, every response that didn't include a piece of data you needed for the next step, every time you wished a related resource was linked from the current one. Those friction points are your priority list.
One powerful technique is including hypermedia links in your responses — URLs that point to related resources or logical next actions. When a developer creates an order, the response could include a link to check that order's status, or to the user who placed it. It's like giving someone a map instead of just an address. They can see where they are and where they can go next. Not every API needs full HATEOAS (a REST architecture style where the server guides the client through available actions), but even a few well-placed links can dramatically improve the developer experience.
The Empathy Shortcut
Every piece of advice in this article really boils down to one thing: treat other developers the way you'd want to be treated. Use your own API before you ship it. Build a sample application with it. Feel the pain points firsthand. Better yet, watch someone else try to use it for the first time without any guidance. The places where they stumble are the places your design needs work.
The best APIs I've ever used didn't feel like APIs at all. They felt like extensions of my own code — natural, predictable, and almost invisible. That's the bar. Not clever. Not cutting-edge. Just thoughtful. Because at 2 AM, when the coffee is cold and the pressure is high, thoughtful design is the only thing that matters.