# 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: ; 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