REST API Design: Hard-Won Lessons from Building 30+ Endpoints
Practical API design principles from someone who has built, versioned, and maintained REST APIs for CRM, ERP, and government systems. The decisions that matter and the ones that don't.
Advertisement
I've designed, built, and later maintained hundreds of REST endpoints across projects ranging from simple CRUD apps to complex government e-service platforms. The most expensive mistakes I've made — and watched others make — aren't about HTTP verbs or status codes. They're about the API contract and how much you respect it.
Version from Day One
Every API I build starts at /api/v1. Not because I expect to reach v2, but because adding versioning after you have consumers is painful. Version in the URL, not in headers — it makes the version visible in logs, bookmarks, and documentation. When you inevitably need to change a contract, /api/v2 can coexist with /api/v1 while clients migrate.
Consistent Response Envelope
{
"data": { ... },
"meta": {
"total": 150,
"page": 1,
"per_page": 20
},
"message": "Success"
}
// Error response — always the same shape
{
"message": "Validation failed",
"errors": {
"email": ["The email field is required."],
"name": ["The name must not exceed 255 characters."]
}
}HTTP Status Codes Worth Getting Right
- 200 OK: successful GET, PUT, PATCH
- 201 Created: successful POST that created a resource — include Location header
- 204 No Content: successful DELETE
- 400 Bad Request: malformed request syntax, not validation errors
- 422 Unprocessable Entity: validation errors (not 400)
- 401 Unauthenticated vs 403 Forbidden — these are not interchangeable
Pagination from the Start
Every collection endpoint gets pagination before it ships. The client that works fine with 50 records will fail with 50,000. Cursor-based pagination scales better than offset-based for large datasets, but offset pagination is easier to implement and works fine for most use cases. The important thing is not to ship an unpaginated list endpoint.
Filter and Sort Conventions
I use a consistent query string convention across all projects: ?filter[status]=active for filters, ?sort=created_at for ascending and ?sort=-created_at for descending (minus prefix). Clients learn the pattern once and it works everywhere. Don't invent bespoke filter syntaxes per endpoint.
The API Contract Is a Promise
Removing a field, changing a field name, or altering a response structure without a version bump is a breaking change. Even if the field 'wasn't documented', if a client is using it you've broken them. Treat every response field as a contract from the moment it ships. Additions are safe; changes and deletions require a new version.
Advertisement