A bad API design is a debt you pay
every time someone integrates with it.
Across roles at CloudFountain, VANtage, and Aspire, I have designed APIs that served internal services, external partners, and third-party integrations including Auth0, DocuSign, and Stripe. Some of those decisions aged well. Others I still think about when I'm debugging a workaround from three years ago.
These are the lessons that actually changed how I design APIs, not the ones from textbooks but the ones from production incidents and integration headaches.
Versioning Is Not Optional
The most avoidable mistake in API design is shipping v1 without a versioning strategy. The moment a consumer exists, you have a contract. Changing it without a version boundary breaks someone's integration, usually at the worst possible time.
Header-based versioning is cleaner in theory but harder to debug, test, and cache correctly. Path versioning (/v1/, /v2/) is visible in logs, browser tools, and documentation. The slight "ugliness" is worth it. We paid the cost of header versioning once and never again.
When you deprecate a version, return a Deprecation response header with a sunset date on every request. Log which consumers are still hitting deprecated endpoints. Give at least 90 days notice for any breaking change. Integrators are busy people. They need visibility and time, not just a changelog entry.
Error Responses Are API Design Too
Every error response should have the same structure: a machine-readable error code, a human-readable message, and optionally a reference ID for support. A consumer should never have to guess whether the error came back as {error: "..."} or {message: "..."} or {errors: [...]}. Inconsistency doubles the integration effort.
400 for invalid input. 401 for missing or invalid authentication. 403 for valid auth but insufficient permissions. 404 for a resource that doesn't exist. 409 for conflicts. 422 for semantically invalid requests. 500 for things that should never happen but did. Returning 200 with an error body in the payload is a pattern that creates bugs in every consumer you have.
Input validation errors should be caught and returned before business logic runs. A 400 with a clear message about which field failed and why is far more useful than a 500 caused by a null reference deep in a service that received bad input.
A well-designed error response is a form of documentation. It tells the consumer exactly what went wrong and, ideally, how to fix it.
Design for the Consumer, Not the Database
If your API resources map 1:1 to your database tables, your schema becomes a public contract. Refactoring your data model then requires a versioned API change. Build a deliberate DTO (Data Transfer Object) layer between your persistence model and your API response shape. The extra mapping code is worth the freedom to evolve internally.
Any endpoint that returns a list should support pagination from day one, even if the initial dataset is small. Adding pagination later is a breaking change if the consumer assumed they would always get the full set. We defaulted to cursor-based pagination for real-time data and offset pagination for admin queries with stable ordering.
Every field you return in a public response is a field you implicitly commit to maintaining. Return the minimum useful payload. Let consumers request expanded fields explicitly if needed. Fields are easy to add; removing them is a breaking change.
The Details That Matter in Practice
Use ISO 8601 for all timestamps and always include timezone information. Return IDs as strings, not integers, even if your database uses integer primary keys. String IDs are safer across languages and don't cause overflow issues in JavaScript.
Document your rate limits explicitly in both the API docs and the response headers. X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset should be returned on every request so consumers can manage their own throttling rather than discovering limits through 429 errors in production.
None of this is particularly novel. The difficulty is consistency: applying these patterns across every endpoint, every service, and every team over time. The best APIs are boring. Boring means predictable, and predictable means your consumers spend time building things instead of debugging surprises.