RESTful API design, error handling, versioning, and best practices. Use when designing APIs, reviewing endpoints, implementing error responses, or setting up API structure...
RESTful API design principles, error handling, pagination, versioning, and security best practices. Guidelines for building consistent, developer-friendly APIs.
Reference these guidelines when:
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Resource Design | CRITICAL | rest- |
| 2 | Error Handling | CRITICAL | error- |
| 3 | Security | CRITICAL | sec- |
| 4 | Pagination & Filtering | HIGH | page- |
| 5 | Versioning | HIGH | ver- |
| 6 | Response Format | MEDIUM | resp- |
| 7 | Documentation | MEDIUM | doc- |
rest-nouns-not-verbs - Use nouns for endpointsrest-plural-resources - Use plural resource namesrest-http-methods - Correct HTTP method usagerest-nested-resources - Proper resource nestingrest-status-codes - Appropriate status codesrest-idempotency - Idempotent operationsrest-hateoas - Hypermedia linkserror-consistent-format - Consistent error structureerror-meaningful-messages - Helpful error messageserror-validation-details - Field-level validation errorserror-codes - Machine-readable error codeserror-stack-traces - Never expose in productionsec-authentication - Proper auth implementationsec-authorization - Resource-level permissionssec-rate-limiting - Prevent abusesec-input-validation - Validate all inputsec-cors - CORS configurationsec-sensitive-data - Protect sensitive datapage-cursor-based - Cursor pagination for large datasetspage-offset-based - Offset pagination for simple casespage-consistent-params - Consistent parameter namespage-metadata - Include pagination metadatafilter-query-params - Filter via query parameterssort-flexible - Flexible sorting optionsver-url-path - Version in URL pathver-header-based - Version in headersver-backward-compatible - Maintain compatibilityver-deprecation - Deprecation strategyresp-consistent-structure - Consistent response enveloperesp-json-conventions - JSON naming conventionsresp-partial-responses - Field selectionresp-compression - Response compressiondoc-openapi - OpenAPI/Swagger specdoc-examples - Request/response examplesdoc-changelog - API changelog# ❌ Verbs in URLs - RPC style
GET /getUsers
POST /createUser
PUT /updateUser/123
DELETE /deleteUser/123
# ✅ Nouns with HTTP methods - REST style
GET /users # List users
POST /users # Create user
GET /users/123 # Get user
PUT /users/123 # Update user (full)
PATCH /users/123 # Update user (partial)
DELETE /users/123 # Delete user
# ✅ Logical nesting (max 2 levels)
GET /users/123/orders # User's orders
GET /users/123/orders/456 # Specific order
POST /users/123/orders # Create order for user
# ❌ Too deeply nested
GET /users/123/orders/456/items/789/reviews
# ✅ Flatten when appropriate
GET /order-items/789/reviews # Direct access
# GET - Retrieve resource(s)
GET /users → 200 OK + array
GET /users/123 → 200 OK + object
GET /users/999 → 404 Not Found
# POST - Create resource
POST /users → 201 Created + object + Location header
POST /users (invalid) → 400 Bad Request + errors
# PUT - Full update (replace)
PUT /users/123 → 200 OK + updated object
PUT /users/999 → 404 Not Found
# PATCH - Partial update
PATCH /users/123 → 200 OK + updated object
# DELETE - Remove resource
DELETE /users/123 → 204 No Content
DELETE /users/999 → 404 Not Found
# Other common status codes
401 Unauthorized # Not authenticated
403 Forbidden # Authenticated but not authorized
409 Conflict # Resource state conflict
422 Unprocessable # Validation failed
429 Too Many Requests # Rate limited
500 Internal Error # Server error
503 Service Unavailable # Maintenance/overload
// ❌ Inconsistent error formats
{ "error": "Not found" }
{ "message": "Invalid email" }
{ "errors": ["Error 1", "Error 2"] }
// ✅ Consistent error envelope
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid data",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Please provide a valid email address"
},
{
"field": "password",
"code": "TOO_SHORT",
"message": "Password must be at least 8 characters"
}
],
"request_id": "req_abc123"
}
}
// ✅ Not found error
{
"error": {
"code": "NOT_FOUND",
"message": "User with ID 123 not found",
"request_id": "req_def456"
}
}
// ✅ Server error (no sensitive details)
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again.",
"request_id": "req_ghi789"
}
}
// ✅ Offset-based pagination
GET /users?page=2&per_page=20
{
"data": [...],
"meta": {
"current_page": 2,
"per_page": 20,
"total_pages": 10,
"total_count": 195
},
"links": {
"first": "/users?page=1&per_page=20",
"prev": "/users?page=1&per_page=20",
"next": "/users?page=3&per_page=20",
"last": "/users?page=10&per_page=20"
}
}
// ✅ Cursor-based pagination (for large datasets)
GET /users?cursor=eyJpZCI6MTIzfQ&limit=20
{
"data": [...],
"meta": {
"has_more": true,
"next_cursor": "eyJpZCI6MTQzfQ"
},
"links": {
"next": "/users?cursor=eyJpZCI6MTQzfQ&limit=20"
}
}
# ✅ Query parameter filtering
GET /users?status=active
GET /users?role=admin&status=active
GET /users?created_after=2024-01-01
# ✅ Sorting
GET /users?sort=created_at # Ascending (default)
GET /users?sort=-created_at # Descending (prefix with -)
GET /users?sort=last_name,-created_at # Multiple fields
# ✅ Field selection (sparse fieldsets)
GET /users?fields=id,name,email
GET /users/123?fields=id,name,orders
# ✅ Search
GET /users?q=john
GET /users?search=john@example
// ✅ Single resource
GET /users/123
{
"data": {
"id": "123",
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-15T10:30:00Z"
},
"relationships": {
"orders": {
"links": {
"related": "/users/123/orders"
}
}
}
}
}
// ✅ Collection
GET /users
{
"data": [
{ "id": "123", "type": "user", ... },
{ "id": "124", "type": "user", ... }
],
"meta": {
"total_count": 100
},
"links": {
"self": "/users?page=1",
"next": "/users?page=2"
}
}
// ✅ Simpler envelope (also acceptable)
{
"user": {
"id": "123",
"name": "John Doe"
}
}
{
"users": [...],
"pagination": {...}
}
# ✅ URL path versioning (recommended - explicit)
GET /api/v1/users
GET /api/v2/users
# ✅ Header versioning
GET /api/users
Accept: application/vnd.myapi.v2+json
# ✅ Query parameter (simple, but less clean)
GET /api/users?version=2
# ✅ Include rate limit headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1640995200
# ✅ Rate limited response
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Please retry after 60 seconds.",
"retry_after": 60
}
}
# For non-CRUD actions, use sub-resources or actions
# ✅ Sub-resource style
POST /users/123/activate
POST /orders/456/cancel
POST /posts/789/publish
# ✅ Or controller-style for complex operations
POST /auth/login
POST /auth/logout
POST /auth/refresh
POST /password/reset
POST /password/reset/confirm
// ✅ Validate and return all errors at once
POST /users
{
"email": "invalid",
"password": "short"
}
// Response: 422 Unprocessable Entity
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address"
},
{
"field": "password",
"code": "TOO_SHORT",
"message": "Must be at least 8 characters",
"meta": {
"min_length": 8,
"actual_length": 5
}
}
]
}
}
// ✅ Include links for discoverability
{
"data": {
"id": "123",
"status": "pending",
"total": 99.99
},
"links": {
"self": "/orders/123",
"cancel": "/orders/123/cancel",
"pay": "/orders/123/pay",
"items": "/orders/123/items"
},
"actions": {
"cancel": {
"method": "POST",
"href": "/orders/123/cancel"
},
"pay": {
"method": "POST",
"href": "/orders/123/pay",
"fields": [
{ "name": "payment_method", "type": "string", "required": true }
]
}
}
}
// ✅ Bulk create
POST /users/bulk
{
"users": [
{ "name": "User 1", "email": "user1@example.com" },
{ "name": "User 2", "email": "user2@example.com" }
]
}
// Response with partial success
{
"data": {
"succeeded": [
{ "id": "123", "name": "User 1" }
],
"failed": [
{
"index": 1,
"error": {
"code": "DUPLICATE_EMAIL",
"message": "Email already exists"
}
}
]
},
"meta": {
"total": 2,
"succeeded": 1,
"failed": 1
}
}
When auditing APIs, output findings in this format:
endpoint - [category] Description of issue
Example:
GET /getUsers - [rest] Use noun '/users' instead of verb '/getUsers'
POST /users - [error] Missing validation error details in 400 response
GET /users - [page] Missing pagination metadata in list response
Read individual rule files for detailed explanations:
rules/rest-http-methods.md
rules/error-consistent-format.md
rules/page-cursor-based.md