REST API Design
Design REST API endpoints that follow HTTP semantics and handle edge cases gracefully.
Decision Points
HTTP Method Selection
Does the request read data without side effects?
├─ YES → GET
│ └─ Never mutate state. Cache-safe.
└─ NO → Does it create a new resource?
├─ YES → Server assigns ID?
│ ├─ YES → POST to collection (/orders)
│ └─ NO → PUT to specific path (/orders/123)
└─ NO → Does it replace entire resource?
├─ YES → PUT to item (/orders/123)
│ └─ Send full representation
└─ NO → Does it update specific fields?
├─ YES → PATCH to item (/orders/123)
│ └─ Send only changed fields
└─ NO → DELETE the resource
└─ Must be idempotent
Status Code Selection
Operation succeeded?
├─ YES → Returning data?
│ ├─ YES → 200 OK
│ ├─ Created resource? → 201 Created + Location header
│ ├─ Async processing? → 202 Accepted + status URL
│ └─ No content? → 204 No Content
└─ NO → Client error or server error?
├─ CLIENT → Malformed request?
│ ├─ Bad JSON/headers → 400 Bad Request
│ ├─ Valid syntax, bad data → 422 Unprocessable Entity
│ ├─ No auth → 401 Unauthorized
│ ├─ Insufficient permissions → 403 Forbidden
│ ├─ Resource missing → 404 Not Found
│ ├─ Conflict/duplicate → 409 Conflict
│ └─ Rate limited → 429 Too Many Requests
└─ SERVER → Code threw?
├─ YES → 500 Internal Server Error
├─ Dependency down? → 502 Bad Gateway
└─ Overloaded? → 503 Service Unavailable
Pagination Strategy
Data volume?
├─ < 100 items → No pagination needed
├─ 100-10k items → Offset-based
│ └─ ?page=1&limit=20
└─ > 10k or real-time → Cursor-based
└─ ?cursor=abc&limit=50
Resource Nesting
Resource relationship?
├─ Independent resource → Top-level (/orders)
├─ Owned by parent → One level nesting (/users/123/orders)
└─ Deeper hierarchy needed?
└─ STOP → Flatten with query params
└─ /comments?postId=123 not /posts/123/comments
Failure Modes
1. Verbs-in-URLs Syndrome
- Symptoms:
/getUsers,/createOrder,/deleteItem/5 - Detection: URL path contains action words (get, create, delete, update)
- Fix: HTTP method IS the verb. Use nouns:
GET /users,POST /orders,DELETE /items/5
2. Deep Nesting Hell
- Symptoms:
/users/1/posts/2/comments/3/likes/4 - Detection: More than 2 path segments with IDs
- Fix: Flatten to
/likes?commentId=3. If resource has global ID, make it top-level
3. Status Code Soup
- Symptoms:
{"error": "Not found"}with HTTP 200, or{"success": false}with 200 - Detection: Error responses use 200 status with error in body
- Fix: HTTP status code IS the outcome signal. Use 404, 422, 409, etc.
4. Pagination Inconsistency
- Symptoms:
/users?page=1but/posts?offset=0&count=20 - Detection: Different pagination params across endpoints
- Fix: Standardize on ONE pattern. Document in API guidelines. Apply everywhere
5. POST Double-Submit Vulnerability
- Symptoms: User clicks twice, gets duplicate orders. Network retry creates duplicates
- Detection: POST endpoints have no idempotency protection
- Fix: Accept
Idempotency-Keyheader. Cache response by(userId, key)for 24h
6. Schema Over-Nesting
- Symptoms:
{"data": {"result": {"items": [{"item": {"value": 123}}]}}} - Detection: More than 2 levels of nesting in response objects
- Fix: Flatten structure. Use consistent envelope:
{"data": [...], "meta": {}}
7. Missing Error Details
- Symptoms:
{"error": "Validation failed"}with no field-level info - Detection: 422 responses lack specific field errors
- Fix: Include
detailsarray withpathandmessagefor each validation failure
8. Unbounded Responses
- Symptoms:
GET /usersreturns all 50,000 users in one response - Detection: Collection endpoints with no pagination or limits
- Fix: Default pagination (limit=20). Max limit (100). Never return unbounded arrays
Worked Examples
Scenario: E-commerce Order Management API
Requirements: Create orders, list user orders, update shipping address, cancel orders.
Step 1: Method Selection
- List orders: Reading data →
GET /orders - Create order: Creating resource →
POST /orders - Update address: Partial update →
PATCH /orders/{id} - Cancel order: Action → Model as resource →
POST /orders/{id}/cancellation
Step 2: Handle Edge Cases
// POST /orders - Creation
{
"items": [{"productId": "prod_1", "quantity": 2}],
"shippingAddress": {...},
"idempotencyKey": "uuid-12345" // Prevent double-submit
}
// Success: 201 Created + Location header
{
"id": "ord_123",
"status": "pending",
"total": 59.98,
"createdAt": "2024-01-15T10:30:00Z"
}
// Duplicate: 409 Conflict (idempotency key reused within 24h)
{
"error": "Order already exists with this idempotency key",
"code": "DUPLICATE_ORDER",
"existingOrderId": "ord_123"
}
// Invalid data: 422 Unprocessable Entity
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"details": [
{"path": "items[0].quantity", "message": "Must be positive integer"},
{"path": "shippingAddress.zipCode", "message": "Invalid format"}
]
}
Step 3: Pagination & Filtering
// GET /orders?status=pending&page=2&limit=10&sort=createdAt:desc
{
"data": [
{"id": "ord_124", "status": "pending", "total": 29.99, ...},
{"id": "ord_123", "status": "pending", "total": 59.98, ...}
],
"pagination": {
"page": 2,
"limit": 10,
"total": 156,
"totalPages": 16
},
"filters": {
"status": "pending",
"sort": "createdAt:desc"
}
}
Step 4: State Transitions
// PATCH /orders/ord_123 - Update shipping
{"shippingAddress": {"street": "123 New St"}}
// Success if order is pending: 200 OK
{"id": "ord_123", "status": "pending", "shippingAddress": {...}}
// Error if already shipped: 409 Conflict
{
"error": "Cannot modify shipped order",
"code": "ORDER_ALREADY_SHIPPED",
"currentStatus": "shipped"
}
// POST /orders/ord_123/cancellation - Cancel order
{"reason": "Changed mind"}
// Success: 201 Created
{
"id": "canc_456",
"orderId": "ord_123",
"reason": "Changed mind",
"refundAmount": 59.98,
"createdAt": "2024-01-15T11:00:00Z"
}
What novice misses: Uses DELETE /orders/123 for cancellation (loses cancel reason/refund data). Returns 200 for all errors. No idempotency protection.
What expert catches: Models cancellation as resource creation. Uses proper status codes. Protects against double-submit. Validates state transitions.
Quality Gates
- Every endpoint uses nouns in paths (no verbs like
/getUsers) - Consistent HTTP method usage (GET=read, POST=create, PATCH=partial update)
- All mutation endpoints return proper status codes (201/204/409/422, never 200 for errors)
- Collection endpoints have pagination with consistent params across API
- Error responses include
error,code, anddetailsfields in standard envelope - POST endpoints that create resources accept
Idempotency-Keyheader - Resource nesting limited to 2 levels maximum (/users/123/orders, not deeper)
- All endpoints documented with request/response examples including error cases
- Rate limiting strategy defined (requests per minute by endpoint type)
- Authentication requirements specified per endpoint (public/authenticated/admin)
NOT-FOR Boundaries
Don't use REST API design for:
- GraphQL APIs → Use
graphql-schema-designskill - WebSocket/SSE real-time APIs → Use
realtime-api-designskill - Platform-specific APIs (Vercel Edge, Cloudflare Workers) → Use platform-specific skills
- Internal RPC between microservices → Use
grpc-designor message queues - File upload/download APIs → Use
file-api-designskill for multipart handling
Delegate to other skills:
- Database schema design → Use
database-designskill - Authentication implementation → Use
auth-implementationskill - OpenAPI documentation → Use
openapi-documentationskill - Performance optimization → Use
api-performanceskill