An API is the interface between systems, and like any interface, its design determines how easy it is to use correctly and how hard it is to use incorrectly. Over the years, we have developed a set of API design principles that we apply to every project. These are not theoretical ideals. They are pragmatic rules derived from building and maintaining APIs that serve real users.
Resource naming follows a consistent convention across all our APIs. Resources are plural nouns: /users, /orders, /invoices. Relationships are expressed through nesting: /users/123/orders. Actions that do not map cleanly to CRUD operations use a verb sub-resource: /orders/123/cancel. We avoid deeply nested URLs (more than two levels) because they create tight coupling between resources and make the API harder to evolve.
A well-designed API is a contract with your future self and every developer who will consume it.
Every API response follows a consistent envelope structure. Successful responses include a data field containing the resource or collection, a meta field with pagination information for collections, and optionally an included field for related resources. Error responses include an error object with a machine-readable code, a human-readable message, and a details array for validation errors. This consistency means API consumers can write generic error handling code that works across all endpoints.
Error Handling and Responses
Versioning is handled through URL path prefixes: /v1/users, /v2/users. We considered header-based versioning but found that path-based versioning is simpler to implement, easier to test, and more visible in logs and documentation. When introducing a breaking change, we maintain the previous version for a minimum of twelve months, clearly document the deprecation timeline, and provide a migration guide. Non-breaking additions, such as new optional fields, do not require a version increment.
Pagination is implemented using cursor-based pagination for all collection endpoints. Offset-based pagination breaks when items are inserted or deleted between requests. Cursor-based pagination provides stable results regardless of concurrent modifications. Each paginated response includes a next_cursor value that the client passes in the subsequent request. We set a default page size of 25 and a maximum of 100.
- Use plural nouns for resources and consistent naming conventions
- Implement a standard response envelope for both success and error responses
- Version through URL paths and maintain deprecated versions for at least twelve months
- Use cursor-based pagination instead of offset-based for stable results
- Generate documentation from OpenAPI specs as part of the CI pipeline
- Treat documentation gaps with the same severity as functional bugs
Documentation is generated from the code using OpenAPI specifications. Every endpoint is documented with request parameters, request body schema, response schema, example responses, and error codes. This documentation is auto-generated in CI and published alongside each deployment. We treat documentation gaps as bugs, with the same priority as functional defects.
