What Makes a Good API: Lessons From the Ones Developers Actually Love

Rachel Stein

Rachel Stein

July 7, 2026

What Makes a Good API: Lessons From the Ones Developers Actually Love

Developers have strong opinions about APIs they’ve worked with. The ones they love tend to share certain qualities that are hard to fully specify but easy to recognize in practice — they feel predictable, they don’t surprise you, they let you accomplish things with less code than you expected, and when something goes wrong, they tell you what went wrong in a way that actually helps you fix it. The ones they hate tend to fail in consistent ways that are also hard to fully specify but easy to catalog: inconsistent naming, unhelpful errors, documentation that doesn’t match behavior, and endpoint designs that seem to reflect internal implementation rather than the problems developers are trying to solve.

There’s real learning in examining what separates the loved APIs from the ones that generate complaints. It’s not just aesthetic preference — it reflects principles that have practical consequences for developer productivity and adoption.

Consistency Above Almost Everything

The quality that developers cite most often when describing a good API is consistency — the property that similar things work similarly, and different things have predictable differences. Consistency means that if you’ve learned how one part of an API works, your knowledge transfers to other parts. You can make educated guesses about endpoint names, parameter formats, and response structures before looking them up, and those guesses are often right.

Stripe’s API is the reference example developers mention most often when asked to name an API they genuinely like. Its consistency is not accidental: Stripe maintains API conventions across its entire surface area. List endpoints always paginate the same way (cursor-based pagination with the same parameter names). Error responses always have the same structure (code, message, type, param). Object names in responses are consistent with object names in documentation. When you learn the pattern from one part of the Stripe API, it works everywhere.

The opposite — an inconsistent API — forces developers to constantly check documentation for things they should be able to remember. Some endpoints return arrays, some return objects. Some use snake_case, some use camelCase. Some paginate with page numbers, others with cursors, others with offset/limit. Each inconsistency is a small cognitive tax that compounds across the lifetime of building on a platform. Inconsistency is often the result of APIs growing organically over time with different teams making different decisions, which is why deliberate API governance matters more as APIs grow.

Backend developer designing REST API schema on a laptop, clean JSON response structure displayed, professional coding environment

Design for the Task, Not the Implementation

A common failure mode in API design is exposing the structure of the underlying data model rather than designing endpoints around the tasks developers are trying to accomplish. APIs that feel like direct database queries — where you’re fetching and assembling raw entities and doing the business logic yourself — put more work on the consumer and often reveal internal implementation details that shouldn’t be part of the external interface.

GitHub’s API is a case where the design generally reflects task-orientation: you can get a pull request with its status, review state, and associated checks in one call rather than assembling it from separate calls to commits, reviews, and checks tables. The API’s structure follows the workflow developers need — “what’s the state of this PR?” — rather than the internal data model’s structure.

REST API over-fetching and under-fetching problems — where a single endpoint returns far more data than needed, or where accomplishing a task requires many sequential calls — are the practical consequence of database-centric design. GraphQL was partly a response to this problem, allowing consumers to specify exactly what data they need. Whether GraphQL is the right solution depends on the use case (it adds client-side complexity while reducing over/under-fetching), but the underlying problem it addresses — APIs that don’t fit their consumers’ access patterns — is real.

Error Responses That Actually Help

The gap between good and bad API error handling is striking. A bad error response gives you an HTTP 400 with a body that says something like “Bad Request” or, worse, “An error occurred.” A good error response tells you which field failed validation, what the constraint was, what value was provided, and ideally what a correct value looks like or where to find the documentation for it.

Twilio’s error handling is frequently cited as strong: each error has a code, a human-readable message, and a documentation URL that links to a page with more detail about that specific error, common causes, and solutions. When you get a Twilio error, you have a path to resolution. When you get a generic “something went wrong” error from a poorly designed API, you’re debugging by trial and error.

Error granularity matters too. Returning HTTP 400 for all validation errors, or HTTP 500 for all server errors, loses information. A 422 Unprocessable Entity is more precise for validation failures than a 400 Bad Request. A 429 Too Many Requests with Retry-After headers lets clients handle rate limiting automatically. HTTP status codes exist for a reason; using them accurately reduces the amount of error-specific logic consumers have to write.

Validation errors that report all failures in a single response are more useful than errors that stop at the first failure. If a developer is testing a new API call, getting one error at a time for each of five invalid fields requires five attempts to get a valid request. Getting all five errors at once requires one attempt.

Documentation That Matches Reality

Documentation that’s out of sync with actual API behavior is one of the most corrosive things that can happen to developer trust. When the documentation says an endpoint returns a field and the field isn’t there, or when the documentation shows an example response that doesn’t reflect current response structure, developers learn not to trust the documentation — which means they have to verify everything experimentally, which is slow and frustrating.

The best API documentation is generated from the same source of truth as the API itself. OpenAPI (formerly Swagger) specifications, when maintained accurately, produce documentation that stays synchronized with implementation because the spec is the specification that the implementation is validated against. APIs that generate documentation from code (using docstrings, type annotations, and automated spec generation) tend to maintain tighter synchronization than APIs with hand-written documentation updated separately from code changes.

Runnable examples matter disproportionately. Documentation with curl examples, code snippets in multiple languages, and particularly interactive “try it” interfaces that let developers make real API calls from the documentation reduce the time from “reading docs” to “working code” dramatically. The Stripe API reference, the Twilio API reference, and the GitHub API reference are all examples of documentation that developers find genuinely useful rather than something to scroll past to get to Stack Overflow.

Developer testing API endpoints in a web browser interface, response JSON displayed with status codes and request headers

Versioning and Stability

Breaking changes are the developer’s nightmare. An API that silently changes behavior, removes fields, or changes response structures breaks production code without warning. The APIs developers trust most are the ones that treat backward compatibility as a serious commitment.

Stripe’s approach to API versioning is instructive: each API key is pegged to a specific API version at creation time, and Stripe will never break that version for an existing key. New features go in new versions, and customers opt into upgrades on their own timeline. This means Stripe carries the complexity of maintaining multiple API versions simultaneously, which is expensive — but it means developers can build on Stripe with confidence that their integration won’t break when Stripe releases new features.

Not every API can achieve Stripe-level versioning commitment, but the principle — treating backward compatibility as a first-class concern rather than an afterthought — is achievable at various levels of strictness. At minimum: versioning the API explicitly (v1, v2 in the URL or Accept headers), deprecating old behavior with sufficient warning and migration paths, and never removing or changing fields in existing versions without a breaking-change process that gives consumers time to adapt.

The developers who advocate most strongly for APIs they love are often citing exactly these properties: the API didn’t break their code, it told them clearly what went wrong when something failed, it behaved consistently so they could learn the patterns and work quickly, and the documentation they needed was accurate and complete. These qualities are achievable by any team that treats API design as a genuine product discipline rather than an implementation detail.

More articles for you