Good API design is mostly about predictability: a consumer who has seen one of your endpoints should be able to guess the rest. The principles below are not academic; each one exists because breaking it forces every consumer to write special-case code, and that code outlives whoever wrote the API.
Design the Resource Model First#
Endpoints fall out of the resource model, not the other way around. Name resources as plural nouns (/orders, /orders/42/items), keep nesting shallow (two levels is usually the ceiling), and resist verbs in paths. If you find yourself writing /getActiveOrders, the missing piece is a filter, not a new endpoint: /orders?status=active.
Use HTTP Semantics, Don't Reinvent Them#
The methods and status codes already mean things; an API that respects them gets client-side caching, retries, and tooling for free.
| Method | Use for | Safe to retry |
|---|---|---|
| GET | Read, no side effects | Yes |
| POST | Create, or actions that aren't idempotent | No |
| PUT | Full replace, idempotent | Yes |
| PATCH | Partial update | Depends |
| DELETE | Remove, idempotent | Yes |
Status codes follow the same rule: 200/201/204 for success, 400 for a malformed request, 401 vs 403 for "who are you" vs "you can't", 404 for missing, 409 for conflicts, 422 for valid syntax but failed validation, 5xx only for your faults, never the client's. Returning 200 with {"error": ...} in the body breaks every generic client ever written.
One Error Shape, Everywhere#
Pick a single error envelope and never deviate:
{"error": {"code": "ORDER_NOT_FOUND","message": "Order 42 does not exist","field": null}}
{"error": {"code": "ORDER_NOT_FOUND","message": "Order 42 does not exist","field": null}}
A machine-readable code lets consumers branch without parsing prose, and a stable shape means one error handler instead of one per endpoint.
Version From Day One#
You will break compatibility eventually; decide how before the first consumer ships. Path versioning (/v1/orders) is the bluntest and the easiest to operate. Within a version, additive changes (new optional fields, new endpoints) are safe; removing or renaming anything is a new version. Publish a deprecation window and honor it.
Paginate and Filter by Default#
Any collection endpoint that returns "all of them" is a future outage. Cursor-based pagination (?cursor=...&limit=50) stays correct when rows are inserted mid-scroll; offset pagination is simpler and fine for admin UIs. Either way, cap the limit server-side and return the next cursor or page token in the response, not in a header consumers will miss.
Security Is a Design Input#
Authenticate every endpoint by default and make exceptions explicit. Scope tokens to the least access that works, and never let the request body set ownership fields (user_id, team_id); identity comes from the session or token, not the payload. Validation belongs at the edge: parse the body against a schema and drop unknown fields before anything else reads them. More depth in API security.
Document the Contract, Then Hold Yourself to It#
An OpenAPI spec is useful exactly as long as it matches production. Generate it from code (or code from it) so drift is impossible, and treat the examples in the docs as test cases: each one should be runnable as-is.
Limitations of Principles#
- Consistency beats purity. If your existing API does something non-ideal everywhere, a "correct" new endpoint that breaks the pattern is worse than repeating the flaw.
- REST is not always the shape. High-fanout internal calls, streaming, and event-driven flows (webhooks) have their own designs; forcing them into resources helps nobody.
- Principles don't catch regressions. A renamed field ships silently unless something outside the codebase is checking the contract.
Conclusion#
Predictable resources, honest HTTP semantics, one error shape, versioning from day one, pagination everywhere: design choices that make an API boring to consume, which is the highest compliment an API gets.
The contract also needs to stay true after every deploy. ObserveOne's API checks call your real endpoints on a schedule from multiple regions and assert on status codes, response time, and JSON fields, so a broken contract pages you before it pages your consumers.