API Design Best Practices: What We've Learned Building 20+ APIs

Practical API design lessons from building over 20 production APIs. Covers REST vs GraphQL, versioning, authentication, rate limiting, and documentation.

Developer typing on keyboard with code on screen
Photo by Christina Morillo on Pexels

APIs Are Contracts, Not Just Endpoints

Over the past several years, we have designed and built more than 20 APIs for clients across industries — from fintech platforms to logistics companies, from SaaS products to internal enterprise tools. Every project has reinforced the same lesson: an API is a contract between systems, and a poorly designed contract creates problems that compound for years.

A well-designed API makes integration easy, reduces support burden, and lets your product evolve without breaking the systems that depend on it. A poorly designed one turns every new feature into a negotiation and every integration partner into a frustrated customer.

Here is what we have learned.

REST vs GraphQL: Choosing the Right Tool

The REST-versus-GraphQL debate generates more heat than light. The answer is almost always contextual, and we have built production systems with both.

Choose REST when your data model maps cleanly to resources, your consumers are varied and unpredictable (public APIs, third-party integrations), and you want the broadest possible compatibility. REST is well-understood, cache-friendly, and supported by every HTTP client in existence. Most of the APIs we build are RESTful because most business applications deal with clear resource types — users, orders, invoices, products — that map naturally to CRUD operations.

Choose GraphQL when your clients need flexible queries across deeply related data, especially when the frontend team and backend team are part of the same organization. We have seen GraphQL shine in dashboard applications where a single screen might pull data from six different entities. Instead of making six REST calls or building a custom endpoint for every view, the frontend team queries exactly what they need.

The mistake we see most often is choosing GraphQL because it sounds modern, then dealing with the complexity of query optimization, N+1 problems, and caching challenges that REST handles out of the box. Technology choices should solve problems, not create new ones.

Versioning: Plan for Change from Day One

Every API will change. The question is whether you plan for that change or scramble when it happens. We have settled on URL-based versioning (/v1/, /v2/) for most projects because it is the most explicit and easiest for consumers to understand.

Header-based versioning (using Accept headers or custom version headers) is technically cleaner, but it creates confusion for developers who are debugging with a browser or curl. Clarity beats elegance when your API consumers include teams you have never met.

Our versioning rules are straightforward. Minor additions — new optional fields, new endpoints — go into the current version. Breaking changes — removing fields, changing response structures, altering authentication flows — require a new version. We maintain the previous version for a minimum of six months with clear deprecation notices in both the documentation and the response headers.

Authentication: Match the Pattern to the Use Case

We have implemented every major authentication pattern, and each one fits a specific scenario.

API keys work well for server-to-server communication where the consumer is a known, trusted system. They are simple to implement and simple to rotate. We use them for internal microservice communication and for trusted third-party integrations where the consumer’s server makes the calls.

JWT (JSON Web Tokens) are our default for user-facing applications. The token carries user identity and permissions, which means the API can authorize requests without hitting the database on every call. We always set short expiration times (15 minutes for access tokens) paired with longer-lived refresh tokens. A JWT that never expires is a security incident waiting to happen.

OAuth 2.0 is the right choice when your API needs to let users authorize third-party applications to act on their behalf. It is more complex to implement, but it is the established standard for a reason. We have built OAuth flows for platforms that needed to support app marketplaces, and the upfront investment in getting OAuth right saves enormous headaches compared to inventing a custom delegation system.

Rate Limiting: Protect Your API and Your Customers

Every API we ship includes rate limiting, even internal ones. Rate limits protect your infrastructure from abuse, prevent a single misbehaving client from degrading service for everyone else, and give you a natural mechanism for tiered pricing.

We return rate limit information in response headers on every request: X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. This lets consumers monitor their own usage and back off before they hit the wall. An API that silently drops requests without telling the consumer why is a source of bugs, not protection.

For implementation, we use sliding window counters backed by Redis. Fixed windows create thundering herd problems at window boundaries. Token bucket algorithms work well for bursty traffic patterns. The right choice depends on your traffic profile, but sliding windows are a solid default.

Error Handling: Be Specific and Consistent

Nothing frustrates an API consumer more than vague error messages. We follow a consistent error response structure across every API we build:

The response includes an HTTP status code that follows standard conventions (400 for bad requests, 401 for authentication failures, 403 for authorization failures, 404 for missing resources, 422 for validation errors, 429 for rate limits, 500 for server errors). The body includes a machine-readable error code, a human-readable message, and a details array for validation errors that points to specific fields.

We never expose stack traces or internal implementation details in production error responses. That is a security risk. But we do include a request ID in every error response so that when a consumer contacts support, we can trace the exact request through our logs.

Documentation: OpenAPI Is the Starting Point, Not the Finish Line

We generate our API documentation from OpenAPI (Swagger) specifications, but we treat the auto-generated docs as a starting point. Raw schema documentation tells you what the API accepts. It does not tell you how to use it.

Every API we deliver includes a getting-started guide that walks a developer from zero to their first successful request in under five minutes. It includes workflow guides that show how endpoints work together to accomplish real tasks. And it includes example requests and responses for every endpoint, not just the schema definitions.

The best API documentation answers the question “how do I do X?” rather than just “what does endpoint Y accept?” We have found that investing in documentation quality reduces support requests by 40-60% compared to shipping schema docs alone.

Pagination: Choose a Strategy and Stick With It

For most APIs, we use cursor-based pagination. Offset-based pagination (?page=2&limit=20) is simpler to implement but breaks when data is inserted or deleted between requests — the consumer sees duplicates or misses records. Cursor-based pagination uses a pointer to the last item returned, which guarantees consistent results regardless of mutations to the underlying data.

We always include pagination metadata in the response: total count (when feasible), next cursor, and a boolean indicating whether more results exist. For endpoints that return large datasets where total count is expensive to compute, we omit it and rely on the has_more flag instead.

Conclusion

API design is one of those disciplines where the decisions you make in the first week determine how much pain you experience for the next several years. The patterns we follow — explicit versioning, context-appropriate authentication, consistent error handling, honest documentation, and stable pagination — are not theoretical best practices. They are lessons from real projects where we have seen both the cost of getting it wrong and the payoff of getting it right.

If you are planning an API and want to get the foundation right from the start, reach out to us. We would rather help you design it well the first time than refactor it later.

Let's build something great Let's build something great Let's build something great Let's build something great Let's build something great