API Design Principles We Keep Coming Back To
After designing and consuming dozens of APIs across different stacks, these are the principles that consistently produce APIs that developers enjoy using.
Good API design is one of those skills that’s difficult to teach but immediately recognizable in practice. A well-designed API is predictable, forggiving, and self-documenting. A poorly designed one creates friction in every integration. Here are the principles we return to.
Resources, Not Actions
REST APIs should be centered on resources — nouns — not remote procedure calls. The HTTP method provides the action.
✓ POST /reservations (create a reservation)
✓ GET /reservations/:id (get one reservation)
✓ PUT /reservations/:id (update a reservation)
✗ POST /createReservation
✗ POST /getReservationById
✗ POST /updateReservation
When an operation doesn’t map cleanly to CRUD on a single resource, a sub-resource or a transition endpoint is usually the right answer: POST /reservations/:id/cancel is acceptable because cancellation is a transition, not just an update.
Versioning From Day One
APIs are contracts. Once external consumers depend on your API, breaking changes are breaking promises. Version from the first route:
/v1/reservations
/v2/reservations
Even if you’re the only consumer today, building the versioning habit now prevents painful retrofits later. Add the version prefix before your first external integration, not after.
Consistent Error Shapes
Error responses are part of your API design. Clients need to be able to programmatically distinguish error types without parsing error message strings.
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The request contains invalid fields",
"details": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "startDate", "message": "Cannot be in the past" }
]
}
}
A machine-readable code field. A human-readable message. An array of field-level errors for validation failures. This shape handles every error type without requiring clients to parse free-form strings.
Pagination on Every List Endpoint
A list endpoint without pagination is a time bomb. The collection will grow. The response will eventually be too large to serve or too slow to render. Build pagination into every list endpoint from the start.
Cursor-based pagination scales better than offset pagination for large datasets, but offset (page, per_page) is simpler to implement and sufficient for most use cases.
Always return pagination metadata: total count, current page, and whether a next page exists. Clients shouldn’t have to infer these.
Idempotency for Mutations
POST requests are not idempotent by default — submitting the same request twice creates two resources. For operations where duplicate creation is harmful (payments, orders, reservations), support idempotency keys:
POST /charges
Idempotency-Key: a1b2c3d4-...
If you receive the same key twice, return the result of the first request rather than executing again. Store keys with a TTL (24 hours is common).
Hypermedia is Aspirational, Not Required
HATEOAS (Hypermedia as the Engine of Application State) is often cited as a REST requirement. In practice, most productive APIs are REST-ish — they use HTTP semantics correctly but don’t include links to related actions in every response.
Don’t let the perfect be the enemy of the usable. A consistent, predictable API without full hypermedia is dramatically more valuable than an inconsistent one that includes _links.
Good API design is ultimately about empathy for the developer who will consume your API at 11 PM trying to ship a feature before a deadline. Design for them.