REST API design conventions and best practices. Use when designing endpoints, naming resources, choosing HTTP methods, structuring responses, handling errors, or planning API versioning.
Consistent, predictable REST APIs.
# Plural nouns for collections
GET /users
GET /users/{id}
POST /users
# Nested resources for relationships
GET /users/{id}/posts
POST /users/{id}/posts
# Flat when relationship is weak or queryable
GET /posts?author={userId}
/users not /user/order-items not /orderItems/users/{id}/activate → POST /users/{id}/activation/users not /users//users/{id}/posts not /users/{id}/posts/{id}/comments/{id}/likes| Method | Use | Idempotent | Body |
|---|
| GET | Read resource(s) | Yes | No |
| POST | Create resource | No | Yes |
| PUT | Replace resource entirely | Yes | Yes |
| PATCH | Partial update | Yes* | Yes |
| DELETE | Remove resource | Yes | No |
*PATCH is idempotent if applying same patch yields same result.
Create new resource → POST /resources
Get single resource → GET /resources/{id}
Get collection → GET /resources
Replace entire resource → PUT /resources/{id}
Update specific fields → PATCH /resources/{id}
Delete resource → DELETE /resources/{id}
Trigger action → POST /resources/{id}/actions/{action}
| Code | When |
|---|---|
| 200 | GET success, PATCH success, DELETE with body |
| 201 | POST created (include Location header) |
| 204 | DELETE success, PUT success (no body) |
| Code | When |
|---|---|
| 400 | Malformed request, validation failed |
| 401 | Not authenticated |
| 403 | Authenticated but not authorized |
| 404 | Resource not found |
| 409 | Conflict (duplicate, state conflict) |
| 422 | Valid syntax but semantic error |
| 429 | Rate limited |
| Code | When |
|---|---|
| 500 | Unexpected server error |
| 502 | Bad gateway (upstream failed) |
| 503 | Service unavailable (maintenance, overload) |
Consistent structure for all errors:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
}
]
}
}
USER_NOT_FOUND, INSUFFICIENT_BALANCEGET /posts?limit=20&cursor=eyJpZCI6MTAwfQ
Response:
{
"items": [...],
"nextCursor": "eyJpZCI6MTIwfQ",
"hasMore": true
}
GET /posts?limit=20&offset=40
Response:
{
"items": [...],
"total": 253,
"limit": 20,
"offset": 40
}
Cursor > offset when:
# Filtering
GET /posts?status=published&authorId=123
# Multiple values
GET /posts?status=draft,published
# Sorting
GET /posts?sort=createdAt:desc
GET /posts?sort=title:asc,createdAt:desc
# Date ranges
GET /posts?createdAfter=2024-01-01&createdBefore=2024-12-31
# Search
GET /posts?q=typescript
Request:
POST /users
{
"name": "Alice",
"email": "[email protected]"
}
Response: 201 Created
Location: /users/123
{
"id": "123",
"name": "Alice",
"email": "[email protected]",
"createdAt": "2024-01-15T10:30:00Z"
}
Request:
PATCH /users/123
{
"name": "Alice Smith"
}
Response: 200 OK
{
"id": "123",
"name": "Alice Smith",
"email": "[email protected]",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-16T14:00:00Z"
}
POST /users/bulk
{
"operations": [
{ "action": "create", "data": { "name": "Bob" } },
{ "action": "update", "id": "123", "data": { "name": "Alice" } },
{ "action": "delete", "id": "456" }
]
}
Response: 200 OK
{
"results": [
{ "success": true, "id": "789" },
{ "success": true, "id": "123" },
{ "success": false, "error": { "code": "NOT_FOUND" } }
]
}
/v1/users
/v2/users
Accept: application/vnd.api+json; version=2
Always ISO 8601 with timezone:
{
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-16T14:00:00.123Z"
}
"id": "123" not "id": 123userId, postId for foreign keys# Default: IDs only
GET /posts/123
{ "authorId": "456" }
# Expanded
GET /posts/123?expand=author
{
"authorId": "456",
"author": {
"id": "456",
"name": "Alice"
}
}
# Multiple expansions
GET /posts/123?expand=author,comments
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1704067200
Retry-After: 60 (on 429)