I’ve consumed hundreds of APIs and built a few dozen. The difference between a well-designed API and a poorly designed one is the difference between “this makes sense” and “who approved this?”
Use Nouns, Not Verbs
# Bad
GET /getUsers
POST /createUser
DELETE /deleteUser/123
# Good
GET /users
POST /users
DELETE /users/123
The HTTP method (GET, POST, PUT, DELETE) already describes the action. The URL should describe the resource.
Use Consistent Naming
Pick a convention and stick to it everywhere:
# Pick ONE of these (I prefer kebab-case for URLs):
/user-profiles # kebab-case
/user_profiles # snake_case
/userProfiles # camelCase
# JSON response keys - pick ONE:
{ "first_name": "Alice" } # snake_case (common in Python APIs)
{ "firstName": "Alice" } # camelCase (common in JavaScript APIs)
Version Your API From Day One
GET /api/v1/users
GET /api/v2/users
You will need to make breaking changes eventually. Without versioning, those changes break every client immediately. With versioning, you can support old clients while rolling out improvements.
Use Proper HTTP Status Codes
200 - Success (for GET, PUT)
201 - Created (for POST)
204 - No Content (for DELETE)
400 - Bad Request (client sent invalid data)
401 - Unauthorized (not authenticated)
403 - Forbidden (authenticated but not permitted)
404 - Not Found
409 - Conflict (duplicate resource)
422 - Unprocessable Entity (validation failed)
429 - Too Many Requests (rate limited)
500 - Internal Server Error
Don’t return 200 for errors with an error message in the body. That makes client error handling unnecessarily complex.
Pagination Is Not Optional
GET /api/v1/users?page=2&limit=20
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 156,
"pages": 8
}
}
If an endpoint returns a list, it needs pagination. Your database might have 100 records now, but in a year it could have 10 million. Better to design for it upfront.
Error Responses Should Be Useful
// Bad error response
{ "error": "Bad request" }
// Good error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be a positive integer" }
]
}
}
Filtering, Sorting, and Searching
# Filtering
GET /users?status=active&role=admin
# Sorting
GET /users?sort=-created_at # - prefix for descending
# Searching
GET /users?search=alice
# Combining
GET /users?status=active&sort=-created_at&search=alice&page=1&limit=20
Rate Limiting
Include rate limit info in response headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1640000000
A well-designed API is a joy to work with. It’s predictable, consistent, and doesn’t require reading the docs for every endpoint. Invest the time upfront – your API consumers (including your future self) will thank you.
