REST API Design Reference
Comprehensive guide to designing production-grade REST APIs
Table of Contents
- REST Principles
- HTTP Methods
- Status Codes
- URL Design
- Request/Response Formats
- Pagination
- Filtering, Sorting, and Searching
- Versioning
- Error Handling
- HATEOAS and Hypermedia
- Rate Limiting
- Caching
- Authentication and Authorization
- CORS
- API Documentation
- Common Patterns
- Anti-Patterns
- Security Best Practices
- Performance Optimization
- Testing Strategies
REST Principles
What is REST?
REST (Representational State Transfer) is an architectural style for distributed hypermedia systems. It was first defined by Roy Fielding in his doctoral dissertation in 2000.
Core Constraints
1. Client-Server Architecture
Principle: Separation of concerns between client and server.
Client (UI/UX) ←→ Server (Data/Logic)
Benefits:
- Independent evolution of client and server
- Improved scalability
- Better portability across platforms
Example:
// Client: React application
fetch('/api/users')
.then(res => res.json())
.then(users => renderUsers(users));
// Server: Express API
app.get('/api/users', (req, res) => {
res.json(users);
});
2. Statelessness
Principle: Each request contains all information needed to process it. Server stores no client context between requests.
Server does NOT store:
- Session state
- Authentication state (use tokens instead)
- Request context
Client must send:
- Authentication credentials (every request)
- All necessary parameters
- Complete context
Example:
# Stateless (CORRECT)
GET /api/users/123 HTTP/1.1
Authorization: Bearer eyJhbGc...
Accept: application/json
# Stateful (INCORRECT - don't do this)
GET /api/users/current HTTP/1.1
Cookie: session_id=abc123
Benefits:
- Improved scalability (no server-side session storage)
- Simplified server implementation
- Better reliability (no session loss)
- Easier load balancing
Trade-offs:
- Larger request payloads (must send auth every time)
- Client manages more state
3. Cacheability
Principle: Responses must define themselves as cacheable or non-cacheable.
Cache Control Headers:
# Cacheable response
HTTP/1.1 200 OK
Cache-Control: max-age=3600, public
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT
# Non-cacheable response
HTTP/1.1 200 OK
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Caching Strategies:
- Public Caching (CDN, shared proxies):
Cache-Control: public, max-age=86400
- Private Caching (browser only):
Cache-Control: private, max-age=3600
- No Caching:
Cache-Control: no-store
- Conditional Requests:
# Request
GET /api/users/123
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# Response (not modified)
HTTP/1.1 304 Not Modified
4. Uniform Interface
Principle: Standardized way to interact with resources.
Four Sub-Constraints:
- Resource Identification:
GET /api/users/123 # Identifies a specific user
GET /api/orders/456/items # Identifies items in an order
- Resource Manipulation through Representations:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
- Self-Descriptive Messages:
GET /api/users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer token
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 98
- HATEOAS (Hypermedia as the Engine of Application State):
{
"id": 123,
"name": "John Doe",
"_links": {
"self": { "href": "/api/users/123" },
"orders": { "href": "/api/users/123/orders" },
"avatar": { "href": "/api/users/123/avatar" }
}
}
5. Layered System
Principle: Client cannot tell if connected directly to server or through intermediaries.
Client → Load Balancer → API Gateway → Cache → Service → Database
Benefits:
- Add authentication layers
- Add caching layers
- Add load balancing
- Enforce security policies
Example Architecture:
┌──────────┐
│ Client │
└────┬─────┘
│
┌────▼──────────┐
│ Load Balancer │
└────┬──────────┘
│
┌────▼────────┐
│ API Gateway │ (Auth, Rate Limiting)
└────┬────────┘
│
┌────▼──────┐
│ Cache │ (Redis, Varnish)
└────┬──────┘
│
┌────▼────────┐
│ API Service │
└────┬────────┘
│
┌────▼────────┐
│ Database │
└─────────────┘
6. Code on Demand (Optional)
Principle: Server can extend client functionality by transferring executable code.
Examples:
- JavaScript sent to browser
- Applets
- Client-side scripts
{
"data": {...},
"script": "https://cdn.example.com/widget.js"
}
Note: This constraint is optional and rarely used in modern REST APIs.
HTTP Methods
Overview
HTTP methods define the action to be performed on a resource.
| Method | CRUD | Idempotent | Safe | Cacheable |
|---|---|---|---|---|
| GET | Read | Yes | Yes | Yes |
| POST | Create | No | No | Rarely |
| PUT | Replace | Yes | No | No |
| PATCH | Update | No | No | No |
| DELETE | Delete | Yes | No | No |
| HEAD | Headers | Yes | Yes | Yes |
| OPTIONS | Metadata | Yes | Yes | No |
Definitions:
- Idempotent: Multiple identical requests have the same effect as a single request
- Safe: Does not modify server state
- Cacheable: Response can be stored for future use
GET - Read Resources
Purpose: Retrieve resource representation.
Characteristics:
- Safe (no side effects)
- Idempotent
- Cacheable
- Can include query parameters
Usage:
# Get single resource
GET /api/users/123
Accept: application/json
# Get collection
GET /api/users?limit=10&offset=0
# Get nested resource
GET /api/users/123/orders
# Get with filtering
GET /api/products?category=electronics&price_min=100
# Get with sorting
GET /api/users?sort=created_at:desc
Response:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=3600
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2025-10-27T10:00:00Z"
}
Best Practices:
# Good: Use query params for filtering
GET /api/users?role=admin&status=active
# Bad: Don't use request body
GET /api/users
Body: { "role": "admin" } # WRONG
# Good: Support field selection
GET /api/users?fields=id,name,email
# Good: Support expansion
GET /api/orders/123?expand=customer,items
POST - Create Resources
Purpose: Create new resource or trigger action.
Characteristics:
- Not safe (modifies state)
- Not idempotent (creates new resource each time)
- Can be cacheable (with appropriate headers)
Usage:
# Create new resource
POST /api/users
Content-Type: application/json
{
"name": "Jane Smith",
"email": "jane@example.com",
"role": "user"
}
Success Response:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/124
{
"id": 124,
"name": "Jane Smith",
"email": "jane@example.com",
"role": "user",
"created_at": "2025-10-27T10:30:00Z"
}
POST for Actions:
# Trigger action
POST /api/users/123/send-welcome-email
Content-Type: application/json
{
"template": "welcome_v2",
"language": "en"
}
Response:
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"job_id": "abc-123",
"status": "queued",
"estimated_completion": "2025-10-27T10:35:00Z"
}
Best Practices:
# Good: Return created resource
HTTP/1.1 201 Created
Location: /api/users/124
Body: { "id": 124, ... }
# Good: Return 202 for async operations
HTTP/1.1 202 Accepted
Body: { "job_id": "abc-123", "status": "processing" }
# Bad: Don't use POST when PUT/PATCH is appropriate
POST /api/users/123/update # WRONG - use PUT/PATCH
# Good: Use POST for complex searches
POST /api/search
Body: { "query": {...}, "filters": {...} }
PUT - Replace Resources
Purpose: Replace entire resource or create at specific URI.
Characteristics:
- Not safe (modifies state)
- Idempotent (same result regardless of repetition)
- Complete replacement
Usage:
# Replace entire resource
PUT /api/users/123
Content-Type: application/json
{
"name": "John Doe Updated",
"email": "john.updated@example.com",
"role": "admin",
"bio": "New bio"
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"name": "John Doe Updated",
"email": "john.updated@example.com",
"role": "admin",
"bio": "New bio",
"updated_at": "2025-10-27T11:00:00Z"
}
Create with PUT (if URI is known):
PUT /api/users/new-user-id
Content-Type: application/json
{
"name": "New User",
"email": "new@example.com"
}
Response:
HTTP/1.1 201 Created
Location: /api/users/new-user-id
Best Practices:
# Good: Full replacement
PUT /api/users/123
Body: { "name": "...", "email": "...", "role": "..." }
# Bad: Partial update (use PATCH instead)
PUT /api/users/123
Body: { "email": "new@example.com" } # Missing fields
# Good: Idempotent behavior
PUT /api/users/123 # First call: updates
PUT /api/users/123 # Second call: same result
# Good: Use for upsert operations
PUT /api/config/theme
Body: { "primary_color": "#007bff" }
PATCH - Partial Update
Purpose: Apply partial modifications to resource.
Characteristics:
- Not safe (modifies state)
- Can be idempotent (depends on implementation)
- Partial modification
Usage:
# JSON Patch (RFC 6902)
PATCH /api/users/123
Content-Type: application/json-patch+json
[
{ "op": "replace", "path": "/email", "value": "new@example.com" },
{ "op": "add", "path": "/phone", "value": "+1234567890" }
]
Merge Patch (RFC 7396):
PATCH /api/users/123
Content-Type: application/merge-patch+json
{
"email": "new@example.com",
"bio": "Updated bio"
}
Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"name": "John Doe",
"email": "new@example.com",
"phone": "+1234567890",
"bio": "Updated bio",
"updated_at": "2025-10-27T11:30:00Z"
}
JSON Patch Operations:
[
{ "op": "add", "path": "/tags/-", "value": "important" },
{ "op": "remove", "path": "/deprecated" },
{ "op": "replace", "path": "/status", "value": "active" },
{ "op": "move", "from": "/old_field", "path": "/new_field" },
{ "op": "copy", "from": "/source", "path": "/destination" },
{ "op": "test", "path": "/version", "value": 2 }
]
Best Practices:
# Good: Use PATCH for partial updates
PATCH /api/users/123
Body: { "email": "new@example.com" }
# Good: Specify content type
Content-Type: application/merge-patch+json
Content-Type: application/json-patch+json
# Bad: Don't use for full replacement
PATCH /api/users/123
Body: { "name": "...", "email": "...", ...all fields... } # Use PUT
# Good: Support both formats
Accept: application/json-patch+json, application/merge-patch+json
DELETE - Remove Resources
Purpose: Delete resource.
Characteristics:
- Not safe (modifies state)
- Idempotent (deleting deleted resource has same effect)
Usage:
# Delete resource
DELETE /api/users/123
Success Response (deleted):
HTTP/1.1 204 No Content
Success Response (with body):
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"deleted": true,
"deleted_at": "2025-10-27T12:00:00Z"
}
Already Deleted:
HTTP/1.1 404 Not Found
Soft Delete:
DELETE /api/users/123
HTTP/1.1 200 OK
{
"id": 123,
"status": "deleted",
"deleted_at": "2025-10-27T12:00:00Z"
}
Best Practices:
# Good: Return 204 No Content
HTTP/1.1 204 No Content
# Good: Return 404 if already deleted
HTTP/1.1 404 Not Found
# Good: Consider soft deletes
DELETE /api/users/123
Response: { "status": "deleted", "recoverable_until": "..." }
# Good: Bulk delete
DELETE /api/users?ids=1,2,3
Response: { "deleted_count": 3 }
# Bad: Don't require request body
DELETE /api/users
Body: { "id": 123 } # WRONG
HEAD - Get Headers
Purpose: Retrieve response headers without body.
Characteristics:
- Safe
- Idempotent
- Same headers as GET
Usage:
HEAD /api/users/123
Response:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 298
Last-Modified: Wed, 27 Oct 2025 10:00:00 GMT
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Use Cases:
- Check if resource exists
- Get metadata (size, modification date)
- Check cache validity
- Pre-flight checks
Example:
# Check if file exists before downloading
response = requests.head('https://api.example.com/files/large-file.zip')
if response.status_code == 200:
file_size = int(response.headers['Content-Length'])
if file_size < MAX_SIZE:
download_file()
OPTIONS - Metadata
Purpose: Retrieve supported methods and capabilities.
Characteristics:
- Safe
- Idempotent
- Used for CORS preflight
Usage:
OPTIONS /api/users/123
Response:
HTTP/1.1 200 OK
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
CORS Preflight:
OPTIONS /api/users
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Status Codes
Overview
HTTP status codes indicate the result of a request.
| Range | Category | Meaning |
|---|---|---|
| 1xx | Informational | Request received, processing |
| 2xx | Success | Request successful |
| 3xx | Redirection | Further action needed |
| 4xx | Client Error | Client error |
| 5xx | Server Error | Server error |
1xx Informational
100 Continue:
# Client sends
POST /api/large-upload
Expect: 100-continue
Content-Length: 1000000000
# Server responds
HTTP/1.1 100 Continue
# Client sends body
101 Switching Protocols:
GET /ws
Upgrade: websocket
Connection: Upgrade
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
2xx Success
200 OK:
# General success
GET /api/users/123
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"name": "John Doe"
}
201 Created:
# Resource created
POST /api/users
Body: { "name": "Jane Smith" }
HTTP/1.1 201 Created
Location: /api/users/124
Content-Type: application/json
{
"id": 124,
"name": "Jane Smith"
}
202 Accepted:
# Async processing
POST /api/reports/generate
Body: { "type": "annual", "year": 2025 }
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"job_id": "abc-123",
"status": "processing",
"status_url": "/api/jobs/abc-123"
}
204 No Content:
# Successful delete
DELETE /api/users/123
HTTP/1.1 204 No Content
206 Partial Content:
# Range request
GET /api/files/large-file.bin
Range: bytes=0-1023
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/102400
Content-Length: 1024
[binary data]
3xx Redirection
301 Moved Permanently:
GET /api/v1/users
HTTP/1.1 301 Moved Permanently
Location: /api/v2/users
302 Found (Temporary Redirect):
GET /api/users/current
HTTP/1.1 302 Found
Location: /api/users/123
303 See Other:
POST /api/orders
Body: { "items": [...] }
HTTP/1.1 303 See Other
Location: /api/orders/456
304 Not Modified:
GET /api/users/123
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
HTTP/1.1 304 Not Modified
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
307 Temporary Redirect:
POST /api/login
Body: { "username": "...", "password": "..." }
HTTP/1.1 307 Temporary Redirect
Location: /api/auth/login
308 Permanent Redirect:
POST /api/v1/users
Body: { "name": "..." }
HTTP/1.1 308 Permanent Redirect
Location: /api/v2/users
4xx Client Errors
400 Bad Request:
POST /api/users
Body: { "invalid": "data" }
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "validation_error",
"message": "Invalid request data",
"details": [
{
"field": "email",
"message": "Email is required"
}
]
}
401 Unauthorized:
GET /api/users/123
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="API"
Content-Type: application/json
{
"error": "unauthorized",
"message": "Authentication required"
}
403 Forbidden:
DELETE /api/users/admin
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "forbidden",
"message": "You don't have permission to delete admin users"
}
404 Not Found:
GET /api/users/999
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": "not_found",
"message": "User with id 999 not found"
}
405 Method Not Allowed:
DELETE /api/health
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
{
"error": "method_not_allowed",
"message": "DELETE is not allowed on this endpoint",
"allowed_methods": ["GET", "HEAD", "OPTIONS"]
}
406 Not Acceptable:
GET /api/users/123
Accept: application/xml
HTTP/1.1 406 Not Acceptable
Content-Type: application/json
{
"error": "not_acceptable",
"message": "Server cannot produce application/xml",
"supported_types": ["application/json"]
}
409 Conflict:
POST /api/users
Body: { "email": "existing@example.com" }
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": "conflict",
"message": "User with email existing@example.com already exists"
}
410 Gone:
GET /api/v1/users/123
HTTP/1.1 410 Gone
Content-Type: application/json
{
"error": "gone",
"message": "API v1 is no longer available. Use /api/v2/users/123"
}
415 Unsupported Media Type:
POST /api/users
Content-Type: application/xml
Body: <user>...</user>
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json
{
"error": "unsupported_media_type",
"message": "Content-Type application/xml is not supported",
"supported_types": ["application/json"]
}
422 Unprocessable Entity:
POST /api/users
Body: { "email": "invalid-email", "age": -5 }
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": "validation_error",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "Invalid email format" },
{ "field": "age", "message": "Age must be positive" }
]
}
429 Too Many Requests:
GET /api/users
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1698400000
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again in 60 seconds"
}
5xx Server Errors
500 Internal Server Error:
GET /api/users/123
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "internal_error",
"message": "An unexpected error occurred",
"incident_id": "inc-123456"
}
501 Not Implemented:
TRACE /api/users
HTTP/1.1 501 Not Implemented
Content-Type: application/json
{
"error": "not_implemented",
"message": "TRACE method is not implemented"
}
502 Bad Gateway:
GET /api/users/123
HTTP/1.1 502 Bad Gateway
Content-Type: application/json
{
"error": "bad_gateway",
"message": "Upstream service returned invalid response"
}
503 Service Unavailable:
GET /api/users
HTTP/1.1 503 Service Unavailable
Retry-After: 300
Content-Type: application/json
{
"error": "service_unavailable",
"message": "Service temporarily unavailable. Maintenance in progress"
}
504 Gateway Timeout:
GET /api/reports/slow
HTTP/1.1 504 Gateway Timeout
Content-Type: application/json
{
"error": "gateway_timeout",
"message": "Upstream service timed out"
}
URL Design
Resource Naming
Principles:
- Use nouns (not verbs)
- Use plural forms for collections
- Use kebab-case for multi-word resources
- Be consistent
Good Examples:
GET /api/users
GET /api/users/123
GET /api/users/123/orders
GET /api/blog-posts
GET /api/user-preferences
Bad Examples:
GET /api/getUsers # Don't use verbs
GET /api/user # Use plural
GET /api/users/getById/123 # Don't use verbs
GET /api/blogPosts # Use kebab-case
GET /api/Users # Use lowercase
Resource Hierarchy
Nested Resources:
# User's orders
GET /api/users/123/orders
# Specific order for user
GET /api/users/123/orders/456
# Order items
GET /api/orders/456/items
# Deep nesting (use sparingly)
GET /api/users/123/orders/456/items/789
Best Practices:
# Good: Limit nesting to 2-3 levels
GET /api/users/123/orders
GET /api/users/123/orders/456
# Bad: Too deep
GET /api/organizations/1/departments/2/teams/3/members/4/tasks/5
# Better: Flatten with query params
GET /api/tasks/5?member_id=4&team_id=3
GET /api/tasks?team_id=3&member_id=4
Query Parameters
Filtering:
GET /api/users?status=active
GET /api/users?role=admin&department=engineering
GET /api/products?category=electronics&price_min=100&price_max=500
Sorting:
GET /api/users?sort=created_at
GET /api/users?sort=-created_at # Descending
GET /api/users?sort=last_name,first_name # Multiple fields
Pagination:
GET /api/users?limit=20&offset=0
GET /api/users?page=1&per_page=20
GET /api/users?cursor=abc123
Field Selection:
GET /api/users?fields=id,name,email
GET /api/users?exclude=password,ssn
Expansion:
GET /api/orders?expand=customer
GET /api/orders?expand=customer,items
GET /api/orders?expand=customer.address
Search:
GET /api/users?q=john
GET /api/users?search=john+doe
GET /api/products?q=laptop&category=electronics
Actions and Operations
Use POST for actions:
POST /api/users/123/send-email
POST /api/orders/456/cancel
POST /api/payments/789/refund
POST /api/reports/generate
Alternative: Use status updates:
PATCH /api/orders/456
Body: { "status": "cancelled" }
PATCH /api/tasks/123
Body: { "completed": true }
Complex operations:
# Search with complex criteria
POST /api/search
Body: {
"query": "laptop",
"filters": {
"category": ["electronics", "computers"],
"price": { "min": 500, "max": 2000 },
"brand": ["Dell", "HP"]
}
}
# Batch operations
POST /api/users/batch-update
Body: {
"user_ids": [1, 2, 3],
"updates": { "status": "active" }
}
Versioning in URLs
URI Versioning:
https://api.example.com/v1/users
https://api.example.com/v2/users
Path Versioning:
https://api.example.com/api/v1/users
https://api.example.com/api/v2/users
Best Practices:
# Good: Major version in path
/api/v1/users
/api/v2/users
# Bad: Minor/patch version in path
/api/v1.2.3/users # Too granular
# Good: Use header for minor versions
GET /api/v2/users
API-Version: 2.1
Request/Response Formats
Content Negotiation
Accept Header (request):
GET /api/users/123
Accept: application/json
Accept: application/xml
Accept: application/json, application/xml;q=0.9
Content-Type Header (response):
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"id": 123,
"name": "John Doe"
}
JSON Format
Standard Format:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2025-10-27T10:00:00Z",
"is_active": true,
"roles": ["user", "editor"],
"metadata": {
"last_login": "2025-10-27T09:00:00Z",
"login_count": 42
}
}
Collection Format:
{
"data": [
{ "id": 1, "name": "User 1" },
{ "id": 2, "name": "User 2" }
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 100,
"total_pages": 5
},
"links": {
"self": "/api/users?page=1",
"next": "/api/users?page=2",
"last": "/api/users?page=5"
}
}
Naming Conventions:
- snake_case (Python, Ruby):
{
"user_id": 123,
"first_name": "John",
"created_at": "2025-10-27T10:00:00Z"
}
- camelCase (JavaScript):
{
"userId": 123,
"firstName": "John",
"createdAt": "2025-10-27T10:00:00Z"
}
Best Practice: Choose one and be consistent.
Date/Time Formats
ISO 8601 (recommended):
{
"created_at": "2025-10-27T10:00:00Z",
"updated_at": "2025-10-27T10:30:00+00:00",
"scheduled_for": "2025-10-28T14:00:00-05:00"
}
Unix Timestamp:
{
"created_at": 1698400000,
"updated_at": 1698401800
}
Best Practice: Use ISO 8601 for human readability.
Null vs Empty Values
{
"name": "John Doe",
"middle_name": null, // Value is null
"email": "john@example.com",
"bio": "", // Empty string
"tags": [], // Empty array
"metadata": {} // Empty object
}
Handling Optional Fields:
// Option 1: Include with null
{
"name": "John Doe",
"phone": null
}
// Option 2: Omit entirely
{
"name": "John Doe"
}
Best Practice: Be consistent. Document your choice.
Boolean Values
{
"is_active": true,
"has_premium": false,
"email_verified": true
}
Don't use:
{
"active": "yes", // Use boolean
"verified": 1, // Use boolean
"enabled": "true" // Use boolean (not string)
}
Enumerations
{
"status": "active", // Not: 1, "ACTIVE"
"role": "admin", // Not: "ADMIN", "Admin"
"priority": "high" // Not: 3, "HIGH"
}
Best Practice: Use lowercase strings.
Pagination
Offset-Based Pagination
Request:
GET /api/users?limit=20&offset=40
Response:
{
"data": [...],
"pagination": {
"limit": 20,
"offset": 40,
"total": 500
},
"links": {
"first": "/api/users?limit=20&offset=0",
"prev": "/api/users?limit=20&offset=20",
"self": "/api/users?limit=20&offset=40",
"next": "/api/users?limit=20&offset=60",
"last": "/api/users?limit=20&offset=480"
}
}
Pros:
- Simple implementation
- Easy to jump to specific page
- Total count available
Cons:
- Performance degrades with large offsets
- Inconsistent with real-time data changes
Page-Based Pagination
Request:
GET /api/users?page=3&per_page=20
Response:
{
"data": [...],
"pagination": {
"page": 3,
"per_page": 20,
"total_pages": 25,
"total_items": 500
},
"links": {
"first": "/api/users?page=1&per_page=20",
"prev": "/api/users?page=2&per_page=20",
"self": "/api/users?page=3&per_page=20",
"next": "/api/users?page=4&per_page=20",
"last": "/api/users?page=25&per_page=20"
}
}
Pros:
- User-friendly (page numbers)
- Easy to understand
Cons:
- Same as offset-based
Cursor-Based Pagination
Request:
GET /api/users?cursor=abc123&limit=20
Response:
{
"data": [
{ "id": 41, "name": "User 41" },
{ "id": 42, "name": "User 42" },
...
],
"pagination": {
"limit": 20,
"next_cursor": "def456",
"prev_cursor": "xyz789",
"has_more": true
},
"links": {
"next": "/api/users?cursor=def456&limit=20",
"prev": "/api/users?cursor=xyz789&limit=20"
}
}
Cursor Generation:
import base64
# Encode last item's ID + timestamp
cursor_data = f"{last_id}:{last_timestamp}"
cursor = base64.b64encode(cursor_data.encode()).decode()
Pros:
- Consistent results with real-time changes
- Better performance for large datasets
- No skipped/duplicate items
Cons:
- Can't jump to specific page
- No total count (usually)
Link Header Pagination (RFC 5988)
Response Headers:
HTTP/1.1 200 OK
Link: </api/users?page=1>; rel="first",
</api/users?page=2>; rel="prev",
</api/users?page=4>; rel="next",
</api/users?page=10>; rel="last"
X-Total-Count: 200
X-Page: 3
X-Per-Page: 20
Pros:
- Clean response body
- Standard HTTP headers
Cons:
- Less discoverable
- Not all clients parse Link headers
Keyset Pagination
Request:
GET /api/users?since_id=100&limit=20
Response:
{
"data": [
{ "id": 101, "name": "User 101" },
{ "id": 102, "name": "User 102" }
],
"pagination": {
"since_id": 100,
"max_id": 120,
"limit": 20
}
}
Best For: Infinite scroll, real-time feeds.
Best Practices
# Good: Include metadata
{
"data": [...],
"pagination": {...},
"links": {...}
}
# Good: Consistent parameter names
GET /api/users?limit=20&offset=0
GET /api/posts?limit=20&offset=0
# Good: Sensible defaults
GET /api/users # Default: limit=20, offset=0
# Good: Maximum limits
GET /api/users?limit=1000 # Returns error
{
"error": "limit_exceeded",
"message": "Maximum limit is 100"
}
# Bad: No pagination info
{
"users": [...] # How do I get more?
}
Filtering, Sorting, and Searching
Filtering
Simple Filters:
GET /api/users?status=active
GET /api/users?role=admin
GET /api/products?category=electronics
Multiple Values (OR):
GET /api/users?role=admin,editor
GET /api/products?category=electronics,computers
Multiple Filters (AND):
GET /api/users?status=active&role=admin&department=engineering
Range Filters:
GET /api/products?price_min=100&price_max=500
GET /api/users?created_after=2025-01-01&created_before=2025-12-31
Complex Filters (Query DSL):
POST /api/search/users
Content-Type: application/json
{
"filters": {
"and": [
{ "field": "status", "op": "eq", "value": "active" },
{
"or": [
{ "field": "role", "op": "eq", "value": "admin" },
{ "field": "role", "op": "eq", "value": "editor" }
]
},
{ "field": "age", "op": "gte", "value": 18 }
]
}
}
LHS Brackets Notation (advanced):
GET /api/users?filter[status]=active&filter[role][in]=admin,editor
GET /api/products?filter[price][gte]=100&filter[price][lte]=500
Sorting
Single Field:
GET /api/users?sort=created_at
GET /api/users?sort=-created_at # Descending
Multiple Fields:
GET /api/users?sort=last_name,first_name
GET /api/users?sort=-priority,created_at
Explicit Direction:
GET /api/users?sort=created_at:asc
GET /api/users?sort=created_at:desc,name:asc
Complex Sorting:
POST /api/search/users
{
"sort": [
{ "field": "priority", "order": "desc" },
{ "field": "created_at", "order": "asc" }
]
}
Searching
Full-Text Search:
GET /api/users?q=john+doe
GET /api/products?search=laptop
Field-Specific Search:
GET /api/users?name=john&email=doe
GET /api/products?name_contains=laptop
Wildcard Search:
GET /api/users?name=john*
GET /api/users?email=*@example.com
Advanced Search (POST):
POST /api/search
Content-Type: application/json
{
"query": "laptop",
"filters": {
"category": ["electronics", "computers"],
"price": { "min": 500, "max": 2000 }
},
"sort": [
{ "field": "relevance", "order": "desc" },
{ "field": "price", "order": "asc" }
],
"pagination": {
"page": 1,
"per_page": 20
}
}
Response:
{
"results": [
{
"id": 123,
"name": "Dell Laptop",
"price": 899,
"relevance_score": 0.95
}
],
"facets": {
"category": {
"electronics": 45,
"computers": 32
},
"brand": {
"Dell": 12,
"HP": 8,
"Lenovo": 7
}
},
"pagination": {...}
}
Best Practices
# Good: Support common patterns
GET /api/users?status=active&sort=-created_at&limit=20
# Good: Validate filters
GET /api/users?invalid_field=value
Response: {
"error": "invalid_filter",
"message": "invalid_field is not a valid filter"
}
# Good: Document operators
GET /api/users?age_gte=18&age_lte=65
# Supported: _eq, _ne, _gt, _gte, _lt, _lte, _in, _contains
# Bad: Unclear syntax
GET /api/users?filters=status:active,role:admin
# Good: Use POST for complex queries
POST /api/search
Body: { complex query DSL }
Versioning
Why Version?
Breaking changes:
- Field removal/rename
- Response format changes
- Behavior changes
- New required fields
Non-breaking changes:
- New optional fields
- New endpoints
- Bug fixes
URI Versioning
Format:
https://api.example.com/v1/users
https://api.example.com/v2/users
Pros:
- Simple and clear
- Easy to test
- Browser-friendly
Cons:
- Versioned URLs everywhere
- Cache invalidation
Example:
# Version 1
GET /api/v1/users/123
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
# Version 2
GET /api/v2/users/123
{
"id": 123,
"full_name": "John Doe", # Renamed field
"email": "john@example.com",
"phone": "+1234567890" # New field
}
Header Versioning
Custom Header:
GET /api/users/123
API-Version: 2
Accept Header:
GET /api/users/123
Accept: application/vnd.example.v2+json
Pros:
- Clean URLs
- Better caching
Cons:
- Less discoverable
- Harder to test
Example:
# Request
GET /api/users/123
API-Version: 2
Accept: application/json
# Response
HTTP/1.1 200 OK
API-Version: 2
Content-Type: application/json
{
"id": 123,
"full_name": "John Doe"
}
Query Parameter Versioning
Format:
GET /api/users/123?version=2
Pros:
- Simple to implement
- Easy to test
Cons:
- Pollutes query namespace
- Not RESTful
Content Negotiation Versioning
Accept Header:
GET /api/users/123
Accept: application/vnd.example.v2+json
Response:
HTTP/1.1 200 OK
Content-Type: application/vnd.example.v2+json
{
"id": 123,
"full_name": "John Doe"
}
Pros:
- RESTful
- Flexible
Cons:
- Complex
- Hard to discover
Versioning Strategy
Semantic Versioning:
Major.Minor.Patch
2.1.0
- Major: Breaking changes
- Minor: New features (backward compatible)
- Patch: Bug fixes
API Versioning:
# Only major version in URL/header
GET /api/v2/users
# Minor version in header (optional)
API-Version: 2.1
Best Practices:
# Good: Version only on breaking changes
/api/v1/users # Initial version
/api/v2/users # Breaking changes
/api/v3/users # More breaking changes
# Bad: Version on every change
/api/v1.0.0/users
/api/v1.0.1/users # Just a bug fix
/api/v1.1.0/users # Minor feature
# Good: Support multiple versions
/api/v1/users # Still supported
/api/v2/users # Current
/api/v3/users # Beta
# Good: Deprecation warnings
GET /api/v1/users
Response:
{
"data": [...],
"deprecation": {
"version": "v1",
"sunset_date": "2026-01-01",
"migration_guide": "https://docs.example.com/migration/v1-to-v2"
}
}
Deprecation Process
Phase 1: Announce:
GET /api/v1/users
HTTP/1.1 200 OK
Warning: 299 - "API v1 is deprecated. Use v2. See https://docs.example.com"
Sunset: Wed, 01 Jan 2026 00:00:00 GMT
Phase 2: Deprecate:
GET /api/v1/users
HTTP/1.1 200 OK
Deprecation: true
Warning: 299 - "API v1 will be removed on 2026-01-01"
Phase 3: Sunset:
GET /api/v1/users
HTTP/1.1 410 Gone
{
"error": "version_deprecated",
"message": "API v1 is no longer available. Use v2",
"migration_guide": "https://docs.example.com/migration/v1-to-v2"
}
Error Handling
Error Response Format
Standard Format:
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": [
{
"field": "email",
"code": "invalid_email",
"message": "Email format is invalid"
}
],
"request_id": "req-123456",
"timestamp": "2025-10-27T10:00:00Z"
}
}
Simple Format:
{
"error": "not_found",
"message": "User with id 123 not found"
}
RFC 7807 - Problem Details
Format:
{
"type": "https://example.com/problems/validation-error",
"title": "Request validation failed",
"status": 422,
"detail": "The request body contains invalid data",
"instance": "/api/users",
"invalid_params": [
{
"name": "email",
"reason": "Invalid email format"
}
]
}
Headers:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
Error Codes
Standard Codes:
# Authentication/Authorization
"unauthorized" # 401
"forbidden" # 403
"token_expired" # 401
"invalid_token" # 401
# Validation
"validation_error" # 422
"invalid_input" # 400
"missing_field" # 400
"invalid_format" # 400
# Resources
"not_found" # 404
"conflict" # 409
"gone" # 410
# Rate Limiting
"rate_limit_exceeded" # 429
"quota_exceeded" # 429
# Server Errors
"internal_error" # 500
"service_unavailable" # 503
"gateway_timeout" # 504
Validation Errors
Detailed Format:
{
"error": "validation_error",
"message": "Request validation failed",
"details": [
{
"field": "email",
"code": "invalid_email",
"message": "Email format is invalid",
"value": "invalid-email"
},
{
"field": "age",
"code": "out_of_range",
"message": "Age must be between 18 and 120",
"value": -5,
"constraints": {
"min": 18,
"max": 120
}
}
]
}
Error Context
Include Helpful Information:
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded",
"context": {
"limit": 100,
"remaining": 0,
"reset_at": "2025-10-27T11:00:00Z",
"retry_after": 60
}
}
Incident Tracking:
{
"error": "internal_error",
"message": "An unexpected error occurred",
"incident_id": "inc-123456",
"support_email": "support@example.com"
}
Best Practices
# Good: Consistent error format
{
"error": "...",
"message": "...",
"details": [...]
}
# Bad: Inconsistent
{
"err": "...",
"msg": "...",
"errors": [...]
}
# Good: User-friendly messages
{
"error": "validation_error",
"message": "Email address is required"
}
# Bad: Technical messages
{
"error": "NullPointerException",
…(truncated)