DEV Community

Michael Sun
Michael Sun

Posted on • Originally published at novvista.com

8 API Design Mistakes That Will Haunt You in Production

APIs are permanent. That sounds dramatic until you've spent three weeks coordinating a breaking change across fourteen client teams while keeping the old endpoint alive in a compatibility shim nobody wants to maintain. Bad code can be refactored quietly. A bad API contract is a public debt that compounds interest every quarter.

Here are eight patterns drawn from designs that looked reasonable at the time, shipped to production, and then caused real pain.

Mistake 1: Not Versioning From Day One

The argument against early versioning sounds rational — you don't know what will change yet. The problem is that "later" arrives the moment a single external client goes to production. At that point, adding versioning requires a migration, not just a decision.

URL path versioning (/v1/users, /v2/users) is the most explicit approach and the right default for most teams. It's easy to route at the infrastructure level and visible in any log file. Stripe uses it for good reason: its clarity is a significant part of why Stripe's API is considered one of the best-designed in the industry.

Mistake 2: Inconsistent Naming Conventions

Mixing camelCase and snake_case in the same API is documentation debt. It forces consumers to resolve the ambiguity by trial and error. The choice of convention matters less than the consistency — pick one and enforce it at the serialization layer, not through convention and hope.

Mistake 3: Returning Too Much Data by Default

An endpoint that returns everything it knows about a resource seems generous. In practice, it's a performance liability. When your /users/{id} returns the profile, preferences, billing history, and activity log, every mobile client pays the serialization cost for data it immediately discards.

The N+1 problem is the related trap: an /orders endpoint that fetches a full customer object per order will issue one query for the order list and one per order for the customer. Pagination should be the default, not an option — and for time-series data, cursor-based pagination is more reliable than offset-based.

Mistake 4: Poor Error Responses

A generic 500 with no body is an absence of information dressed as a response. Stripe's error format is the standard worth emulating: HTTP status code, machine-readable code field, human-readable message, and enough context to reproduce the problem. Validation errors should identify which fields failed and why, not just "validation failed."

Mistake 5: No Rate Limiting

An API without rate limits is an invitation to unintentional denial-of-service. A client with a buggy retry loop can bring down your service for everyone. The sliding window counter algorithm is the right default: approximates the accuracy of a full sliding log with the memory efficiency of fixed windows. Expose limits in response headers (X-RateLimit-Remaining, Retry-After) so well-behaved clients can adapt.

Mistake 6: Ignoring Idempotency

Network requests fail in ways that are indistinguishable from success. A timeout — did the server process it before responding failed? For write operations, a blind retry can create duplicate charges or inconsistent state. Stripe requires idempotency keys for all POST requests that modify financial data. Implement them before you have external clients, not after your first support ticket about a duplicate charge.

Mistake 7: Authentication as an Afterthought

API keys are appropriate for server-to-server communication. OAuth 2.0 is appropriate when acting on behalf of users. JWT is a token format, not an authentication scheme — and treating it as automatically secure is how you end up with the none algorithm vulnerability. Make the authentication decision before your first external consumer, because retrofitting it is painful.

Mistake 8: Coupling Internal Models to API Responses

Serializing database rows directly into API responses is efficient until your next refactoring. Rename a column and you've shipped a breaking change. Add an internal field like password_hash and you risk accidental exposure. Data Transfer Objects — explicit serializer classes that define the shape of each response — create a deliberate boundary between internal implementation and external contract.

Three Key Takeaways

  1. The structural decisions that can't wait are versioning, error format, authentication model, and the boundary between internal model and external contract. Retrofitting these is expensive. Everything else can be improved iteratively.
  2. API design failures are externalized. The pain of poor design is paid by consumers on a timeline you don't control. That asymmetry justifies more upfront care than most internal engineering decisions.
  3. Study Stripe, GitHub, and Twilio. Not because they're famous, but because their design choices are deliberate and documented. The patterns they use for consistency, pagination, error handling, and versioning exist for good reasons.

Read the full article at novvista.com for a detailed breakdown of each mistake, real implementation examples, and analysis of how Stripe, GitHub, and Twilio set the standard.


Originally published at NovVista

Top comments (0)