Design clean, intuitive REST APIs with proper HTTP methods, status codes, versioning, error formats, pagination, and authentication patterns.
A good API is predictable, consistent, and self-explanatory. Developers shouldn't need to read documentation to guess what GET /users/123 does. Here are the practices that separate professional APIs from confusing ones.
Resources are things, not actions. The HTTP method expresses the action.
✓ GET /articles
✓ GET /articles/42
✓ POST /articles
✓ PUT /articles/42
✓ DELETE /articles/42
✗ GET /getArticles
✗ POST /createArticle
✗ GET /deleteArticle?id=42
| Method | Meaning | Idempotent? |
|---|---|---|
| GET | Retrieve resource | Yes |
| POST | Create resource | No |
| PUT | Replace entire resource | Yes |
| PATCH | Partial update | No |
| DELETE | Delete resource | Yes |
Use PATCH for partial updates, not PUT. PUT replaces the entire resource.
200 OK — successful GET, PATCH, PUT
201 Created — successful POST (include Location header)
204 No Content — successful DELETE
400 Bad Request — invalid input (include error details)
401 Unauthorized — missing/invalid auth token
403 Forbidden — authenticated but not authorized
404 Not Found — resource doesn't exist
409 Conflict — e.g., email already taken
422 Unprocessable — validation errors
429 Too Many Requests — rate limited
500 Internal Server Error — unexpected server error
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
}
]
}
}
Always return errors in the same shape so clients can handle them generically.
Include the version in the URL path:
/api/v1/users
/api/v2/users
Never break existing clients. When you need breaking changes, create a new version. Maintain v1 until clients migrate.
GET /articles?page=2&limit=20
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 347,
"totalPages": 18,
"hasNext": true,
"hasPrev": true
}
}
For large datasets, cursor-based pagination is more efficient:
GET /articles?cursor=eyJpZCI6MTAwfQ&limit=20
{
"data": [...],
"nextCursor": "eyJpZCI6MTIwfQ",
"hasMore": true
}
GET /articles?status=published&author=alex
GET /articles?sort=created_at:desc
GET /articles?fields=id,title,author
GET /users/42/orders
GET /users/42/orders/99
POST /users/42/orders
Don't nest more than two levels deep — it gets unwieldy.
Stick to one convention throughout. camelCase for JSON (JavaScript convention):
{
"userId": 42,
"firstName": "Alex",
"createdAt": "2025-04-01T10:00:00Z"
}
Use ISO 8601 for all dates.
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Never pass tokens in query strings (they appear in server logs).
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 43
X-RateLimit-Reset: 1714953600
Retry-After: 30
Good API design is about empathy for the developer using your API. Consistent naming, correct status codes, predictable error formats, and thoughtful versioning make the difference between an API people love and one they dread. Write the API you'd want to consume.