APIs are permanent. That sounds dramatic until you have spent three weeks coordinating a breaking change across fourteen client teams while keeping the old endpoint alive in a compatibility shim that nobody wants to maintain and everyone is afraid to delete. The permanence of APIs — the way they calcify once clients depend on them — is what makes API design mistakes production lessons worth studying with genuine seriousness. Bad code can be refactored quietly. A bad API contract is a public debt that compounds interest every quarter.

The eight patterns below are drawn from designs that looked reasonable at the time, shipped to production, and then caused real pain when the audience grew or requirements changed. They are not academic mistakes. They are the decisions that seemed fine on a Friday afternoon and became incident tickets on a Tuesday morning.

Mistake 1: Not Versioning From Day One

The argument against adding versioning before you need it sounds rational: you do not know yet what will change, adding /v1/ to every URL feels premature, and you can always add it later. The problem is that “later” arrives the moment a single external client goes to production, and at that point you are not free to add versioning without a migration. You have already locked yourself in.

The three common versioning strategies each carry different tradeoffs. URL path versioning/v1/users, /v2/users — is the most explicit. It is easy to route at the infrastructure level, simple to reason about in logs, and the version is visible to anyone reading a URL or a curl command. The cost is that clients must update base URLs on version changes. This is the approach Stripe uses, and its clarity is a significant reason Stripe’s API is considered one of the best-designed in the industry.

Header-based versioning uses a request header like Accept: application/vnd.api+json;version=2 or a custom header such as API-Version: 2024-01-15. GitHub’s REST API uses a date-based header versioning scheme. The advantage is that URLs remain stable; the disadvantage is that versioning becomes invisible in browser address bars and harder to test without proper tooling. Query parameter versioning/users?version=2 — sits in an uncomfortable middle ground that most practitioners have moved away from, primarily because it is easy to cache incorrectly and easy to omit accidentally.

The correct answer for most teams is URL path versioning. Its explicitness prevents the subtle bugs that header-based versioning introduces and the caching problems that query parameters create. Whatever you choose, choose it before your first external client.

Mistake 2: Inconsistent Naming Conventions

Inconsistency is a form of API documentation debt. Every inconsistency in your naming is an ambiguity that consumers must resolve by trial and error or by reading source code they should not need to read. The damage accumulates faster than you expect.

The most common inconsistency is mixing camelCase and snake_case in the same API. This happens when different endpoints are built by different developers or at different times. Clients writing a general-purpose request handler discover the inconsistency only when their snake_case parser silently drops a camelCase field. JSON has no canonical convention — JavaScript contexts tend toward camelCase, Ruby and Python contexts toward snake_case — so the choice matters less than the consistency. Pick one and enforce it in a serialization layer or linter, not through convention and hope.

Plural versus singular endpoint naming is equally important. /user and /users as two different endpoints in the same API — one returning a single user, one returning a collection — create genuine confusion about whether /order exists alongside /orders. The widely adopted REST convention uses plurals for collections (/users, /orders) and a nested identifier for individual resources (/users/{id}, /orders/{id}). This convention exists precisely so developers do not have to memorize whether each resource is singular or plural.

Naming inconsistencies also appear in boolean fields (is_active vs active vs enabled), timestamp fields (created_at vs createdAt vs creation_time), and error field names that differ between endpoints. A style guide is not bureaucracy — it is the mechanism by which an API feels like one coherent product rather than a collection of independent services that happen to share a domain.

Mistake 3: Returning Too Much Data by Default

An API endpoint that returns everything it knows about a resource seems generous. In practice it is a performance liability disguised as convenience. When your /users/{id} endpoint returns the user’s profile, preferences, notification settings, billing history, activity log, and linked social accounts, every client — including the mobile app that only needs the user’s name and avatar — pays the serialization, transmission, and parsing cost for data it immediately discards.

The N+1 problem is a closely related trap. A /orders endpoint that returns a list of orders, where each order contains a full customer object fetched individually from the database, will issue one query for the order list and then one query per order for the customer. Ten orders becomes eleven database queries. A hundred orders becomes a hundred and one. This is not a scaling concern — it is a correctness concern that surfaces the first time anyone calls the endpoint with a realistic dataset.

Missing pagination is perhaps the most reliably recurring API design mistake. A /transactions endpoint that returns all transactions works fine during development with a few hundred records and fails silently or catastrophically in production with a few hundred thousand. Pagination should be the default, not an option. The question is not whether to paginate but which strategy to use: offset-based pagination (?page=2&per_page=50) is familiar and simple but produces incorrect results when records are inserted or deleted during traversal; cursor-based pagination uses an opaque pointer to a position in the result set and is stable under concurrent modifications. For time-series data or feeds where records are frequently added, cursor-based pagination is the correct default. Stripe uses cursor-based pagination throughout its API, which is non-coincidental given the financial context where consistency is critical.

Field selection — allowing clients to specify exactly which fields they need — solves the over-fetching problem at the protocol level. GraphQL makes this the core primitive. REST APIs can implement it with a fields query parameter: /users/{id}?fields=id,name,email. This is not over-engineering; it is the mechanism by which a single API endpoint can serve both a data-hungry dashboard and a bandwidth-constrained mobile application without maintaining separate endpoint variants.

Mistake 4: Poor Error Responses

A generic 500 Internal Server Error with no body is not an error response. It is an absence of information dressed up as a response. Debugging it requires either application logs or guesswork, neither of which are available to external API consumers.

A well-designed error response carries four things: an HTTP status code that accurately reflects the problem category, a machine-readable error code that clients can programmatically handle, a human-readable message that explains what went wrong, and enough context to reproduce or diagnose the problem. Stripe’s error responses are the standard worth emulating. When a payment fails, Stripe returns not just a status code and message but a specific code field (card_declined, insufficient_funds, expired_card), a decline_code with more granular information, and a doc_url pointing to relevant documentation. Each error has a machine-readable identity that client code can respond to specifically, not generically.

The status code selection matters more than developers often assume. Returning 200 OK with a body that contains an error: true field is a mistake that forces every client to parse the body before knowing whether the request succeeded — it defeats the purpose of HTTP status codes entirely. 422 Unprocessable Entity is more appropriate than 400 Bad Request when the request is syntactically valid but semantically incorrect. 409 Conflict is more informative than 400 when a resource already exists. These distinctions are not pedantic — they are the vocabulary that allows clients to handle different failure modes without hard-coding response body parsing for every endpoint.

Validation error responses deserve special attention. When a form submission fails validation, the client needs to know which fields failed and why, not just that “validation failed.” A response that includes a structured errors object keyed by field name, with an array of error messages per field, allows a client application to display inline validation feedback without any additional API calls.

Mistake 5: Authentication as an Afterthought

The decision about authentication strategy is often deferred until the API is functionally complete, at which point it gets bolted on with minimal consideration. This produces authentication schemes that do not match the actual use case, leading to either security problems or unnecessary complexity for consumers.

API keys are appropriate for server-to-server communication where the client is a known, trusted system rather than an end user. They are simple to implement and simple to use — a single secret value in a header. Their weakness is that they are typically long-lived and not scoped, which means a leaked key provides full access until manually rotated. For public-facing APIs used by developers, API keys are a reasonable default if combined with per-key rate limiting and the ability to issue multiple keys per account with different permission scopes.

OAuth 2.0 is appropriate when your API acts on behalf of end users, when third-party applications need delegated access to user data, or when you need granular permission scopes that users explicitly grant. It is meaningfully more complex to implement correctly, but that complexity exists for good reasons — the authorization code flow with PKCE provides protections against interception attacks that simpler schemes do not offer. GitHub’s API uses OAuth for third-party application access and personal access tokens (a sophisticated variant of API keys) for developer tooling. The distinction is intentional.

JWT (JSON Web Tokens) are a token format, not an authentication scheme, though they are often conflated with one. JWTs are useful for stateless authentication where the server needs to avoid a database lookup on every request — the token itself encodes claims that the server can verify cryptographically. The common failure mode is treating JWTs as automatically secure without understanding that the payload is merely base64-encoded and readable by anyone, that the algorithm selection matters significantly (none and HS256 have different security profiles), and that revocation requires either short expiry times or a server-side token store — which reintroduces the statefulness the JWT was meant to eliminate.

Mistake 6: No Rate Limiting

An API without rate limiting is an invitation to unintentional denial-of-service. A client with a bug in a retry loop, a developer accidentally calling an endpoint in a tight loop during testing, a legitimate user with unusually high volume — any of these can degrade the API for everyone without rate limiting in place. This is not a theoretical concern. It is the reason that virtually every public API with more than a handful of consumers has rate limiting.

The two most practical algorithms for production rate limiting are the token bucket and the sliding window. The token bucket algorithm maintains a virtual bucket of tokens that refills at a fixed rate — each request consumes a token, and requests are rejected when the bucket is empty. It handles burst traffic gracefully: a client that has been quiet for a while has accumulated tokens and can make a burst of requests before hitting the limit. This matches the behavior of legitimate high-value clients better than a rigid fixed window.

The sliding window algorithm tracks request counts over a rolling time period rather than fixed intervals. It eliminates the boundary problem of fixed windows — where a client can make double the allowed requests by splitting them across a window boundary — at the cost of slightly more state to maintain. For most APIs, a sliding window with a one-minute or one-hour window is the clearest implementation to reason about and communicate to consumers.

Rate limiting should surface useful information in response headers. Twilio’s API, for example, returns X-Rate-Limit-Limit, X-Rate-Limit-Remaining, and X-Rate-Limit-Reset headers on every response. This allows well-behaved clients to implement adaptive backoff without guessing. The Retry-After header on 429 responses tells clients exactly when they can try again. Publishing rate limit information in response headers converts rate limiting from a blunt instrument into a collaboration between API and client.

Mistake 7: Ignoring Idempotency

Network requests fail in ways that are difficult to distinguish from success. A request times out — did the server receive it and fail to respond, or did the request never arrive? A client disconnects after sending a request — did the server process it before the connection dropped? For read operations, this ambiguity is harmless: retry the request and get the same result. For write operations, especially in financial or inventory contexts, a blind retry can create duplicate records, double charges, or inconsistent state.

Idempotency keys solve this problem by giving a unique identity to each intended operation rather than each HTTP request. The client generates a unique key (typically a UUID) for each operation it intends to perform exactly once, and includes it in a request header such as Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000. The server stores the key and the result of the first successful processing, and returns the stored result for any subsequent request with the same key — without re-processing the operation. Stripe requires idempotency keys for all POST requests that create or modify financial data. This is not an optional nicety; it is a fundamental requirement for building reliable financial integrations.

The implementation requires a durable store for key-result pairs with an appropriate TTL (Stripe retains idempotency keys for 24 hours). The overhead is modest. The alternative — documenting that “you might get duplicate charges in rare network conditions” — is not acceptable for operations where correctness is a business requirement rather than a performance concern.

Mistake 8: Coupling Internal Models to API Responses

The most insidious API design mistake is the one that feels the most efficient: serializing your internal domain objects directly into API responses. The efficiency is real — no mapping layer, no translation code, immediate consistency between internal state and external representation. The cost is that your API contract becomes a direct reflection of your database schema and internal implementation choices, which means any internal refactoring becomes a potential breaking change for external clients.

When your /users endpoint returns the raw database row, renaming a column requires either an API version bump or a coordinated change with every client. When your internal user model starts storing first_name and last_name separately instead of as a single name field, that structural change propagates immediately to API consumers who had no say in the matter. When you add an internal field like password_hash or stripe_customer_id to your model, you risk accidentally exposing it in API responses if your serialization is insufficiently selective.

Data Transfer Objects — DTO classes or serializer modules that explicitly define the shape of API responses — create a deliberate boundary between internal representation and external contract. Every field in an API response exists because someone chose to include it, not because it happened to be present in the internal model. GitHub’s API response objects are notably stable across internal refactors; the serialization layer absorbs internal changes before they reach the API surface. The additional code is not overhead — it is the code that prevents your next database migration from becoming your next API incident.

How Stripe, GitHub, and Twilio Set the Standard

The three API designs cited most frequently as models worth studying did not arrive at their quality accidentally. They made specific, deliberate choices that are worth naming.

Stripe’s API is notable for its consistency above all else. The same patterns — error format, pagination, versioning, idempotency keys — apply across every endpoint. The versioning strategy is date-based (2024-06-20) rather than integer-based, which communicates that each version represents the API as it existed on a specific date rather than an arbitrary increment. New API versions are opt-in per-account, and Stripe maintains old versions for years. The discipline required to sustain this across an API surface that has grown substantially over a decade is significant.

GitHub’s API distinguishes cleanly between its REST API and GraphQL API based on use case. The REST API uses predictable resource-based URLs with consistent hypermedia links. The GraphQL API allows clients to specify exactly the fields they need — addressing the over-fetching problem directly for complex queries that would otherwise require multiple REST requests. Offering both is not indecision; it is recognition that different consumers have genuinely different needs.

Twilio’s API is designed around developer experience from the ground up. Error messages are written for the developer reading them at 2 AM during an incident, not for the lawyer who might later review the documentation. Rate limit headers are present on every response. Webhook payloads include enough context that clients can process events without additional API calls. These details accumulate into an API that developers trust and continue to build on, which is the commercial outcome good API design produces.

The “Good Enough” API: When to Stop Designing and Ship

There is a failure mode on the opposite end of the spectrum from neglect: over-engineering an API before you know what it needs to support. Designing a full hypermedia API with HATEOAS links for a product that has no external clients yet is solving a problem that does not exist while deferring the work of understanding what clients actually need.

The pragmatic threshold for API quality is approximately this: does the API handle the actual requirements correctly, consistently, and without creating traps for consumers? Does it version from day one? Does it have meaningful error responses? Does it paginate collections? Does it authenticate appropriately for the threat model? If yes to these questions, the remaining refinements can be deferred until actual usage reveals what matters.

The trap to avoid is confusing “good enough to ship” with “will not cause pain later.” Rate limiting, idempotency keys, and the DTO boundary are not nice-to-haves — they are the category of decisions that are trivial to implement before the API has significant usage and genuinely expensive to retrofit after the fact. The design work that can safely wait is the polish: comprehensive field selection, advanced query capabilities, sophisticated caching headers. The design work that cannot wait is the structural stuff: versioning, error format, authentication model, and the separation between internal model and external contract.

APIs are one of the few engineering artifacts where the cost of mistakes is paid by people outside your organization, on a timeline you do not control, for longer than you will probably continue to work on this codebase. That asymmetry — where the pain of poor design is externalized and distributed across all your consumers — is the reason API design deserves more attention than most internal engineering decisions. Build it right the first time. Your future self, your consumers, and whoever inherits this codebase will all be grateful.


Frequently Asked Questions

What is the most common API design mistake teams make in production?

Missing versioning is the most universally painful mistake because it is invisible until an external client exists, at which point retrofitting it requires a coordinated migration. Poor error responses are a close second — they are present in nearly every early-stage API and create significant friction during client integration and debugging.

Should REST APIs always use URL path versioning?

URL path versioning is the most widely recommended approach for public APIs because it is explicit, easy to route at the infrastructure layer, and visible in logs and debugging tools. Header-based versioning (as used by GitHub) is a reasonable alternative, particularly for APIs where URL stability matters to consumers. The important thing is to choose a strategy and implement it before any external clients reach production.

When does an API need idempotency keys?

Idempotency keys are essential for any POST operation that creates or modifies state where duplicate processing would produce incorrect results — financial transactions, order creation, account provisioning, and similar critical operations. Read operations are inherently idempotent. Write operations that are safely repeatable (such as setting a preference to a specific value) do not require idempotency keys, though adding them does no harm.

How should rate limits be communicated to API consumers?

Rate limit information should be returned in response headers on every request: the total limit, the remaining quota, and the time at which the quota resets. On 429 Too Many Requests responses, a Retry-After header should indicate when the client may safely retry. Publishing this information allows well-behaved clients to implement adaptive request pacing rather than hitting the limit repeatedly.

What is the difference between authentication and authorization in API design?

Authentication establishes who is making the request — the identity of the caller. Authorization determines what that caller is permitted to do — which resources and actions are available. A common API design mistake conflates the two: adding a new endpoint and assuming that any authenticated caller should have access. Authorization should be explicit, with every endpoint specifying which permission scopes are required, and those scopes communicated clearly in API documentation.

By Michael Sun

Founder and Editor-in-Chief of NovVista. Software engineer with hands-on experience in cloud infrastructure, full-stack development, and DevOps. Writes about AI tools, developer workflows, server architecture, and the practical side of technology. Based in China.

Leave a Reply

Your email address will not be published. Required fields are marked *