REST API Design Principles That Scale
Your API starts with 5 endpoints. Then it grows to 50. Then 200. At some point, the naming is inconsistent, the error responses vary by endpoint, and new developers spend more time reading the API docs than writing code. Good API design prevents this entropy.
URL Structure
Resources are nouns, not verbs:
# GOOD — resources
GET /api/v1/users
GET /api/v1/users/{id}
POST /api/v1/users
PATCH /api/v1/users/{id}
DELETE /api/v1/users/{id}
# BAD — verbs
GET /api/v1/getUsers
POST /api/v1/createUser
POST /api/v1/deleteUser/{id}
Nested resources for relationships:
GET /api/v1/users/{id}/posts # Posts by user
GET /api/v1/posts/{id}/comments # Comments on post
Limit nesting to one level. /users/{id}/posts/{id}/comments/{id}/likes is too deep — flatten to /comments/{id}/likes.
Consistent Response Format
Every endpoint returns the same structure:
{
"data": { ... },
"meta": {
"timestamp": "2024-01-15T12:00:00Z",
"request_id": "req_abc123"
}
}
For collections:
{
"data": [ ... ],
"meta": {
"total": 156,
"page": 1,
"per_page": 20,
"total_pages": 8
}
}
For errors:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{ "field": "email", "message": "Must be a valid email address" }
]
},
"meta": {
"timestamp": "2024-01-15T12:00:00Z",
"request_id": "req_abc123"
}
}
Consistent structure means the frontend can write a single API client that handles all responses.
HTTP Status Codes
Use the correct status codes:
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PATCH |
| 201 | Created | Successful POST that creates a resource |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Validation error, malformed input |
| 401 | Unauthorized | Missing or invalid auth token |
| 403 | Forbidden | Valid auth but insufficient permissions |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Duplicate resource (e.g., email already exists) |
| 422 | Unprocessable | Syntactically correct but semantically invalid |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Error | Unexpected server failure |
Versioning
Prefix all endpoints with a version:
/api/v1/users
/api/v2/users
When you need breaking changes, create a new version. The old version continues to work. Give clients 6 months to migrate with deprecation headers:
Deprecation: true
Sunset: Sat, 01 Jul 2025 00:00:00 GMT
Link: </api/v2/users>; rel="successor-version"
Pagination
Use cursor-based pagination for consistency:
GET /api/v1/posts?cursor=post_abc123&limit=20
Response:
{
"data": [...],
"meta": {
"next_cursor": "post_xyz789",
"has_more": true
}
}
Cursor pagination is stable — inserts and deletes do not cause skipped or duplicated items, unlike offset pagination.
Filtering and Sorting
GET /api/v1/posts?category=backend&published=true&sort=-created_at&limit=20
Conventions:
- Filters as query params:
field=value - Sort with prefix:
-created_at(descending),created_at(ascending) - Limit for page size:
limit=20 - Fields for sparse responses:
fields=id,title,slug
Rate Limiting
Include rate limit info in response headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705315200
PATCH vs. PUT
PUT replaces the entire resource. If you PUT without a field, it is set to null.
PATCH updates only the provided fields. Missing fields remain unchanged.
Use PATCH for updates in most cases. PUT is for complete replacements.
Takeaways
Good API design is about consistency and predictability. When every endpoint follows the same URL structure, returns the same response format, and uses the correct status codes, developers can use the API without memorizing endpoint-specific quirks.
Design your API for the next developer, not just the current feature.