524 lines
14 KiB
Markdown
524 lines
14 KiB
Markdown
|
|
# REST API Design
|
|
|
|
## Overview
|
|
|
|
**REST API design specialist covering resource modeling, HTTP semantics, versioning, pagination, and API evolution.**
|
|
|
|
**Core principle**: REST is an architectural style based on resources, HTTP semantics, and stateless communication. Good REST API design makes resources discoverable, operations predictable, and evolution manageable.
|
|
|
|
## When to Use This Skill
|
|
|
|
Use when encountering:
|
|
|
|
- **Resource modeling**: Designing URL structures, choosing singular vs plural, handling relationships
|
|
- **HTTP methods**: GET, POST, PUT, PATCH, DELETE semantics and idempotency
|
|
- **Status codes**: Choosing correct 2xx, 4xx, 5xx codes
|
|
- **Versioning**: URI vs header versioning, managing API evolution
|
|
- **Pagination**: Offset, cursor, or page-based pagination strategies
|
|
- **Filtering/sorting**: Query parameter design for collections
|
|
- **Error responses**: Standardized error formats
|
|
- **HATEOAS**: Hypermedia-driven APIs and discoverability
|
|
|
|
**Do NOT use for**:
|
|
- GraphQL API design → `graphql-api-design`
|
|
- Framework-specific implementation → `fastapi-development`, `django-development`, `express-development`
|
|
- Authentication patterns → `api-authentication`
|
|
|
|
## Quick Reference - HTTP Methods
|
|
|
|
| Method | Semantics | Idempotent? | Safe? | Request Body | Response Body |
|
|
|--------|-----------|-------------|-------|--------------|---------------|
|
|
| GET | Retrieve resource | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes |
|
|
| POST | Create resource | ❌ No | ❌ No | ✅ Yes | ✅ Yes |
|
|
| PUT | Replace resource | ✅ Yes | ❌ No | ✅ Yes | ✅ Optional |
|
|
| PATCH | Partial update | ❌ No* | ❌ No | ✅ Yes | ✅ Optional |
|
|
| DELETE | Remove resource | ✅ Yes | ❌ No | ❌ Optional | ✅ Optional |
|
|
| HEAD | Retrieve headers | ✅ Yes | ✅ Yes | ❌ No | ❌ No |
|
|
| OPTIONS | Supported methods | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes |
|
|
|
|
*PATCH can be designed to be idempotent but often isn't
|
|
|
|
## Quick Reference - Status Codes
|
|
|
|
| Code | Meaning | Use When |
|
|
|------|---------|----------|
|
|
| 200 OK | Success | GET, PUT, PATCH succeeded with response body |
|
|
| 201 Created | Resource created | POST created new resource |
|
|
| 202 Accepted | Async processing | Request accepted, processing continues async |
|
|
| 204 No Content | Success, no body | DELETE succeeded, PUT/PATCH succeeded without response |
|
|
| 400 Bad Request | Invalid input | Validation failed, malformed request |
|
|
| 401 Unauthorized | Authentication failed | Missing or invalid credentials |
|
|
| 403 Forbidden | Authorization failed | User authenticated but lacks permission |
|
|
| 404 Not Found | Resource missing | Resource doesn't exist |
|
|
| 409 Conflict | State conflict | Resource already exists, version conflict |
|
|
| 422 Unprocessable Entity | Semantic error | Valid syntax but business logic failed |
|
|
| 429 Too Many Requests | Rate limited | User exceeded rate limit |
|
|
| 500 Internal Server Error | Server error | Unexpected server failure |
|
|
| 503 Service Unavailable | Temporary outage | Maintenance, overload |
|
|
|
|
## Resource Modeling Patterns
|
|
|
|
### 1. URL Structure
|
|
|
|
**✅ Good patterns**:
|
|
|
|
```
|
|
GET /users # List users
|
|
POST /users # Create user
|
|
GET /users/{id} # Get specific user
|
|
PUT /users/{id} # Replace user
|
|
PATCH /users/{id} # Update user
|
|
DELETE /users/{id} # Delete user
|
|
|
|
GET /users/{id}/orders # User's orders (nested resource)
|
|
POST /users/{id}/orders # Create order for user
|
|
GET /orders/{id} # Get specific order (top-level for direct access)
|
|
|
|
GET /search/users?q=john # Search endpoint
|
|
```
|
|
|
|
**❌ Anti-patterns**:
|
|
|
|
```
|
|
GET /getUsers # Verb in URL (use HTTP method instead)
|
|
POST /users/create # Redundant verb
|
|
GET /users/123/delete # DELETE operation via GET
|
|
POST /api?action=createUser # RPC-style, not REST
|
|
GET /users/{id}/orders/{id} # Ambiguous - which {id}?
|
|
```
|
|
|
|
### 2. Singular vs Plural
|
|
|
|
**Convention: Use plural for collections, even for single-item endpoints**
|
|
|
|
```
|
|
✅ /users/{id} # Consistent plural
|
|
✅ /orders/{id} # Consistent plural
|
|
|
|
❌ /user/{id} # Inconsistent singular
|
|
❌ /users/{id}/order/{id} # Mixed singular/plural
|
|
```
|
|
|
|
**Exception**: Non-countable resources can be singular
|
|
|
|
```
|
|
✅ /me # Current user context
|
|
✅ /config # Application config (single resource)
|
|
✅ /health # Health check endpoint
|
|
```
|
|
|
|
### 3. Nested Resources vs Top-Level
|
|
|
|
**Nested when showing relationship**:
|
|
|
|
```
|
|
GET /users/{userId}/orders # "Orders belonging to this user"
|
|
POST /users/{userId}/orders # "Create order for this user"
|
|
```
|
|
|
|
**Top-level when resource has independent identity**:
|
|
|
|
```
|
|
GET /orders/{orderId} # Direct access to order
|
|
DELETE /orders/{orderId} # Delete order directly
|
|
```
|
|
|
|
**Guidelines**:
|
|
- Nest ≤ 2 levels deep (`/users/{id}/orders/{id}` is max)
|
|
- Provide top-level access for resources that exist independently
|
|
- Use query parameters for filtering instead of deep nesting
|
|
|
|
```
|
|
✅ GET /orders?userId=123 # Better than /users/123/orders/{id}
|
|
❌ GET /users/{id}/orders/{id}/items/{id} # Too deep
|
|
```
|
|
|
|
## Pagination Patterns
|
|
|
|
### Offset Pagination
|
|
|
|
**Good for**: Small datasets, page numbers, SQL databases
|
|
|
|
```
|
|
GET /users?limit=20&offset=40
|
|
|
|
Response:
|
|
{
|
|
"data": [...],
|
|
"pagination": {
|
|
"limit": 20,
|
|
"offset": 40,
|
|
"total": 1000,
|
|
"hasMore": true
|
|
}
|
|
}
|
|
```
|
|
|
|
**Pros**: Simple, allows jumping to any page
|
|
**Cons**: Performance degrades with large offsets, inconsistent with concurrent modifications
|
|
|
|
### Cursor Pagination
|
|
|
|
**Good for**: Large datasets, real-time data, NoSQL databases
|
|
|
|
```
|
|
GET /users?limit=20&after=eyJpZCI6MTIzfQ
|
|
|
|
Response:
|
|
{
|
|
"data": [...],
|
|
"pagination": {
|
|
"nextCursor": "eyJpZCI6MTQzfQ",
|
|
"hasMore": true
|
|
}
|
|
}
|
|
```
|
|
|
|
**Pros**: Consistent results, efficient for large datasets
|
|
**Cons**: Can't jump to arbitrary page, cursors are opaque
|
|
|
|
### Page-Based Pagination
|
|
|
|
**Good for**: UIs with page numbers
|
|
|
|
```
|
|
GET /users?page=3&pageSize=20
|
|
|
|
Response:
|
|
{
|
|
"data": [...],
|
|
"pagination": {
|
|
"page": 3,
|
|
"pageSize": 20,
|
|
"totalPages": 50,
|
|
"totalCount": 1000
|
|
}
|
|
}
|
|
```
|
|
|
|
**Choice matrix**:
|
|
|
|
| Use Case | Pattern |
|
|
|----------|---------|
|
|
| Admin dashboards, small datasets | Offset or Page |
|
|
| Infinite scroll feeds | Cursor |
|
|
| Real-time data (chat, notifications) | Cursor |
|
|
| Need page numbers in UI | Page |
|
|
| Large datasets (millions of rows) | Cursor |
|
|
|
|
## Filtering and Sorting
|
|
|
|
### Query Parameter Conventions
|
|
|
|
```
|
|
GET /users?status=active&role=admin # Simple filtering
|
|
GET /users?createdAfter=2024-01-01 # Date filtering
|
|
GET /users?search=john # Full-text search
|
|
GET /users?sort=createdAt&order=desc # Sorting
|
|
GET /users?sort=-createdAt # Alternative: prefix for descending
|
|
GET /users?fields=id,name,email # Sparse fieldsets
|
|
GET /users?include=orders,profile # Relationship inclusion
|
|
```
|
|
|
|
### Advanced Filtering Patterns
|
|
|
|
**LHS Brackets (Rails-style)**:
|
|
|
|
```
|
|
GET /users?filter[status]=active&filter[role]=admin
|
|
```
|
|
|
|
**RHS Colon (JSON API style)**:
|
|
|
|
```
|
|
GET /users?filter=status:active,role:admin
|
|
```
|
|
|
|
**Comparison operators**:
|
|
|
|
```
|
|
GET /products?price[gte]=100&price[lte]=500 # Price between 100-500
|
|
GET /users?createdAt[gt]=2024-01-01 # Created after date
|
|
```
|
|
|
|
## API Versioning Strategies
|
|
|
|
### 1. URI Versioning
|
|
|
|
```
|
|
GET /v1/users
|
|
GET /v2/users
|
|
```
|
|
|
|
**Pros**: Explicit, easy to route, clear in logs
|
|
**Cons**: Violates REST principles (resource identity changes), URL proliferation
|
|
|
|
**Best for**: Public APIs, major breaking changes
|
|
|
|
### 2. Header Versioning
|
|
|
|
```
|
|
GET /users
|
|
Accept: application/vnd.myapi.v2+json
|
|
```
|
|
|
|
**Pros**: Clean URLs, follows REST principles
|
|
**Cons**: Less visible, harder to test in browser
|
|
|
|
**Best for**: Internal APIs, clients with header control
|
|
|
|
### 3. Query Parameter Versioning
|
|
|
|
```
|
|
GET /users?version=2
|
|
```
|
|
|
|
**Pros**: Easy to test, optional (can default to latest)
|
|
**Cons**: Pollutes query parameters, not semantic
|
|
|
|
**Best for**: Minor version variants, opt-in features
|
|
|
|
### Version Deprecation Process
|
|
|
|
1. **Announce**: Document deprecation timeline (6-12 months recommended)
|
|
2. **Warn**: Add `Deprecated` header to responses
|
|
3. **Sunset**: Add `Sunset` header with end date (RFC 8594)
|
|
4. **Migrate**: Provide migration guides and tooling
|
|
5. **Remove**: After sunset date, return 410 Gone
|
|
|
|
```
|
|
HTTP/1.1 200 OK
|
|
Deprecated: true
|
|
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
|
|
Link: </v2/users>; rel="successor-version"
|
|
```
|
|
|
|
## Error Response Format
|
|
|
|
**Standard JSON error format**:
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"code": "VALIDATION_ERROR",
|
|
"message": "One or more fields failed validation",
|
|
"details": [
|
|
{
|
|
"field": "email",
|
|
"message": "Invalid email format",
|
|
"code": "INVALID_FORMAT"
|
|
},
|
|
{
|
|
"field": "age",
|
|
"message": "Must be at least 18",
|
|
"code": "OUT_OF_RANGE"
|
|
}
|
|
],
|
|
"requestId": "req_abc123",
|
|
"timestamp": "2024-11-14T10:30:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Problem Details (RFC 7807)**:
|
|
|
|
```json
|
|
{
|
|
"type": "https://api.example.com/errors/validation-error",
|
|
"title": "Validation Error",
|
|
"status": 400,
|
|
"detail": "The request body contains invalid data",
|
|
"instance": "/users",
|
|
"invalid-params": [
|
|
{
|
|
"name": "email",
|
|
"reason": "Invalid email format"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## HATEOAS (Hypermedia)
|
|
|
|
**Level 3 REST includes hypermedia links**:
|
|
|
|
```json
|
|
{
|
|
"id": 123,
|
|
"name": "John Doe",
|
|
"status": "active",
|
|
"_links": {
|
|
"self": { "href": "/users/123" },
|
|
"orders": { "href": "/users/123/orders" },
|
|
"deactivate": {
|
|
"href": "/users/123/deactivate",
|
|
"method": "POST"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Benefits**:
|
|
- Self-documenting API
|
|
- Clients discover available actions
|
|
- Server controls workflow
|
|
- Reduces client-server coupling
|
|
|
|
**Tradeoffs**:
|
|
- Increased response size
|
|
- Complexity for simple APIs
|
|
- Limited client library support
|
|
|
|
**When to use**: Complex workflows, long-lived APIs, discoverability requirements
|
|
|
|
## Idempotency Keys
|
|
|
|
**For POST operations that should be safely retryable**:
|
|
|
|
```
|
|
POST /orders
|
|
Idempotency-Key: key_abc123xyz
|
|
|
|
{
|
|
"items": [...],
|
|
"total": 99.99
|
|
}
|
|
```
|
|
|
|
**Server behavior**:
|
|
1. First request with key → Process and store result
|
|
2. Duplicate request with same key → Return stored result (do not reprocess)
|
|
3. Different request with same key → Return 409 Conflict
|
|
|
|
**Implementation**:
|
|
|
|
```python
|
|
@app.post("/orders")
|
|
def create_order(order: Order, idempotency_key: str = Header(None)):
|
|
if idempotency_key:
|
|
# Check if key was used before
|
|
cached = redis.get(f"idempotency:{idempotency_key}")
|
|
if cached:
|
|
return JSONResponse(content=cached, status_code=200)
|
|
|
|
# Process order
|
|
result = process_order(order)
|
|
|
|
if idempotency_key:
|
|
# Cache result for 24 hours
|
|
redis.setex(f"idempotency:{idempotency_key}", 86400, result)
|
|
|
|
return result
|
|
```
|
|
|
|
## API Evolution Patterns
|
|
|
|
### Adding Fields (Non-Breaking)
|
|
|
|
**✅ Safe changes**:
|
|
- Add optional request fields
|
|
- Add response fields
|
|
- Add new endpoints
|
|
- Add new query parameters
|
|
|
|
**Client requirements**: Ignore unknown fields
|
|
|
|
### Removing Fields (Breaking)
|
|
|
|
**Strategies**:
|
|
1. **Deprecation period**: Mark field as deprecated, remove in next major version
|
|
2. **Versioning**: Create v2 without field
|
|
3. **Optional → Required**: Never safe, always breaking
|
|
|
|
### Changing Field Types (Breaking)
|
|
|
|
**❌ Breaking**:
|
|
- String → Number
|
|
- Number → String
|
|
- Boolean → String
|
|
- Flat → Nested object
|
|
|
|
**✅ Non-breaking**:
|
|
- Number → String (if client coerces)
|
|
- Adding nullability (required → optional)
|
|
|
|
**Strategy**: Add new field with correct type, deprecate old field
|
|
|
|
## Richardson Maturity Model
|
|
|
|
| Level | Description | Example |
|
|
|-------|-------------|---------|
|
|
| 0 | POX (Plain Old XML) | Single endpoint, all operations via POST |
|
|
| 1 | Resources | Multiple endpoints, still using POST for everything |
|
|
| 2 | HTTP Verbs | Proper HTTP methods (GET, POST, PUT, DELETE) |
|
|
| 3 | Hypermedia (HATEOAS) | Responses include links to related resources |
|
|
|
|
**Most APIs target Level 2** (HTTP verbs + status codes).
|
|
**Level 3 is optional** but valuable for complex domains.
|
|
|
|
## Common Anti-Patterns
|
|
|
|
| Anti-Pattern | Why Bad | Fix |
|
|
|--------------|---------|-----|
|
|
| Verbs in URLs (`/createUser`) | Not RESTful, redundant with HTTP methods | Use POST /users |
|
|
| GET with side effects | Violates HTTP semantics, not safe | Use POST/PUT/DELETE |
|
|
| POST for everything | Loses HTTP semantics, not idempotent | Use appropriate method |
|
|
| 200 for errors | Breaks HTTP contract | Use correct 4xx/5xx codes |
|
|
| Deeply nested URLs | Hard to navigate, brittle | Max 2 levels, use query params |
|
|
| Binary response flags | Unclear semantics | Use proper HTTP status codes |
|
|
| Timestamps without timezone | Ambiguous | Use ISO 8601 with timezone |
|
|
| Pagination without total | Can't show "Page X of Y" | Include total count or hasMore |
|
|
|
|
## Best Practices Checklist
|
|
|
|
**Resource Design**:
|
|
- [ ] Resources are nouns, not verbs
|
|
- [ ] Plural names for collections
|
|
- [ ] Max 2 levels of nesting
|
|
- [ ] Consistent naming conventions (snake_case or camelCase)
|
|
|
|
**HTTP Semantics**:
|
|
- [ ] Correct HTTP methods for operations
|
|
- [ ] Proper status codes (not just 200/500)
|
|
- [ ] Idempotent operations are actually idempotent
|
|
- [ ] GET/HEAD have no side effects
|
|
|
|
**API Evolution**:
|
|
- [ ] Versioning strategy defined
|
|
- [ ] Backward compatibility maintained within version
|
|
- [ ] Deprecation headers for sunset features
|
|
- [ ] Migration guides for breaking changes
|
|
|
|
**Error Handling**:
|
|
- [ ] Consistent error response format
|
|
- [ ] Detailed field-level validation errors
|
|
- [ ] Request IDs for tracing
|
|
- [ ] Human-readable error messages
|
|
|
|
**Performance**:
|
|
- [ ] Pagination for large collections
|
|
- [ ] ETags for caching
|
|
- [ ] Gzip compression enabled
|
|
- [ ] Rate limiting implemented
|
|
|
|
## Cross-References
|
|
|
|
**Related skills**:
|
|
- **GraphQL alternative** → `graphql-api-design`
|
|
- **FastAPI implementation** → `fastapi-development`
|
|
- **Django implementation** → `django-development`
|
|
- **Express implementation** → `express-development`
|
|
- **Authentication** → `api-authentication`
|
|
- **API testing** → `api-testing`
|
|
- **API documentation** → `api-documentation` or `muna-technical-writer`
|
|
- **Security** → `ordis-security-architect` (OWASP API Security)
|
|
|
|
## Further Reading
|
|
|
|
- **REST Dissertation**: Roy Fielding's original thesis
|
|
- **RFC 7807**: Problem Details for HTTP APIs
|
|
- **RFC 8594**: Sunset HTTP Header
|
|
- **JSON:API**: Opinionated REST specification
|
|
- **OpenAPI 3.0**: API documentation standard
|