Designing RESTful APIs from scratch
Scope: RESTful resource modeling, HTTP semantics, URL conventions, status codes Lines: ~280 Last Updated: 2025-10-18
Activate this skill when:
REST (Representational State Transfer): Architectural style for distributed systems.
Key principles:
❌ Wrong (verb-based):
POST /createUser
POST /getUserById
POST /updateUserEmail
POST /deleteUser
✅ Correct (resource-based):
POST /users # Create user
GET /users/:id # Get user
PUT /users/:id # Update user
DELETE /users/:id # Delete user
Use plural nouns:
GET /users # ✅ Consistent plural
GET /user # ❌ Singular (inconsistent)
Lowercase with hyphens (not underscores or camelCase):
GET /user-profiles # ✅ Kebab-case
GET /user_profiles # ❌ Snake_case (harder to read in URLs)
GET /userProfiles # ❌ camelCase (inconsistent)
Hierarchy for relationships:
GET /users/:id/posts # User's posts
GET /users/:id/posts/:postId # Specific post by user
GET /posts/:id/comments # Comments on post
Collections vs Singular Resources:
GET /users # Collection (returns array)
GET /users/42 # Singular resource (returns object)
GET /users/me # Special singular (current user)
Purpose: Fetch data without side effects
Characteristics:
Examples:
GET /users # List all users
GET /users?page=2&limit=20 # Paginated list
GET /users/42 # Get user by ID
GET /users/42/posts # Get user's posts
GET /posts?author=alice # Filtered list
Response:
// Single resource
{
"id": 42,
"email": "alice@example.com",
"name": "Alice"
}
// Collection
{
"data": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
],
"meta": {
"total": 100,
"page": 1,
"per_page": 20
}
}
Purpose: Create new resource or trigger action
Characteristics:
Examples:
POST /users # Create new user
POST /posts # Create new post
POST /posts/42/publish # Action (publish post)
Request:
POST /users
Content-Type: application/json
{
"email": "alice@example.com",
"name": "Alice",
"password": "secret123"
}
Response (201 Created):
HTTP/1.1 201 Created
Location: /users/42
Content-Type: application/json
{
"id": 42,
"email": "alice@example.com",
"name": "Alice",
"created_at": "2025-10-18T12:00:00Z"
}
Use POST for non-idempotent actions:
POST /orders/42/payments # Process payment (not idempotent)
POST /users/42/send-email # Send email (side effect)
Purpose: Replace entire resource (or create if doesn't exist)
Characteristics:
Examples:
PUT /users/42 # Replace entire user
PUT /posts/123 # Replace entire post
Request (must include all fields):
PUT /users/42
Content-Type: application/json
{
"email": "alice@example.com",
"name": "Alice Cooper", // Updated
"bio": "Developer" // All fields required
}
Response (200 OK):
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 42,
"email": "alice@example.com",
"name": "Alice Cooper",
"bio": "Developer",
"updated_at": "2025-10-18T12:00:00Z"
}
PUT for upsert (create if not exists):
PUT /users/alice@example.com # Create or replace by email
Purpose: Partially update resource (modify specific fields)
Characteristics:
Examples:
PATCH /users/42 # Update specific fields
PATCH /posts/123 # Update subset of post
Request (only modified fields):
PATCH /users/42
Content-Type: application/json
{
"name": "Alice Cooper" // Only update name
}
Response (200 OK):
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 42,
"email": "alice@example.com",
"name": "Alice Cooper", // Updated
"bio": "Developer", // Unchanged
"updated_at": "2025-10-18T12:00:00Z"
}
PUT vs PATCH:
PUT /users/42 # Replace entire user (all fields required)
PATCH /users/42 # Update specific fields (partial update)
Purpose: Delete resource
Characteristics:
Examples:
DELETE /users/42 # Delete user
DELETE /posts/123 # Delete post
Response (204 No Content):
HTTP/1.1 204 No Content
Or (200 OK with body):
HTTP/1.1 200 OK
Content-Type: application/json
{
"message": "User deleted successfully",
"id": 42
}
Soft delete (use PATCH instead):
PATCH /users/42
{"deleted_at": "2025-10-18T12:00:00Z"}
200 OK: Standard success response (GET, PUT, PATCH)
GET /users/42 → 200 OK + user object
PUT /users/42 → 200 OK + updated user
PATCH /users/42 → 200 OK + updated user
201 Created: Resource created successfully (POST)
POST /users → 201 Created + new user
Location: /users/42
204 No Content: Success with no response body (DELETE)
DELETE /users/42 → 204 No Content
206 Partial Content: Partial resource (range requests)
GET /videos/123
Range: bytes=0-1024 → 206 Partial Content
301 Moved Permanently: Resource permanently moved
GET /old-endpoint → 301 Moved Permanently
Location: /new-endpoint
304 Not Modified: Cached version is still valid
GET /users/42
If-None-Match: "abc123" → 304 Not Modified
400 Bad Request: Invalid request (validation failed)
POST /users
{"email": "invalid"} → 400 Bad Request
{
"error": "Validation failed",
"details": {
"email": ["Must be valid email address"]
}
}
401 Unauthorized: Authentication required or failed
GET /users/me → 401 Unauthorized
{
"error": "Authentication required",
"message": "Missing or invalid token"
}
403 Forbidden: Authenticated but not authorized
DELETE /users/99 → 403 Forbidden
{
"error": "Forbidden",
"message": "You don't have permission to delete this user"
}
404 Not Found: Resource doesn't exist
GET /users/99999 → 404 Not Found
{
"error": "Not found",
"message": "User not found"
}
409 Conflict: Request conflicts with current state
POST /users
{"email": "alice@example.com"} → 409 Conflict
{
"error": "Conflict",
"message": "User with this email already exists"
}
422 Unprocessable Entity: Valid syntax, but semantic errors
POST /orders
{"product_id": 999} → 422 Unprocessable Entity
{
"error": "Unprocessable entity",
"message": "Product not found"
}
429 Too Many Requests: Rate limit exceeded
GET /users → 429 Too Many Requests
Retry-After: 60
{
"error": "Rate limit exceeded",
"message": "Try again in 60 seconds"
}
500 Internal Server Error: Unexpected server error
GET /users → 500 Internal Server Error
{
"error": "Internal server error",
"message": "An unexpected error occurred"
}
502 Bad Gateway: Upstream server error (proxy/gateway)
GET /users → 502 Bad Gateway
{
"error": "Bad gateway",
"message": "Upstream service unavailable"
}
503 Service Unavailable: Server temporarily unavailable
GET /users → 503 Service Unavailable
Retry-After: 120
{
"error": "Service unavailable",
"message": "Server is under maintenance"
}
GET /users/:userId/posts # User's posts
GET /users/:userId/posts/:postId # Specific post
GET /users/:userId/posts/:postId/comments # Post's comments
Limit nesting to 2-3 levels:
✅ GET /posts/:postId/comments/:commentId
❌ GET /users/:userId/posts/:postId/comments/:commentId/replies/:replyId
Filtering:
GET /posts?status=published&author=alice
GET /users?role=admin&verified=true
Sorting:
GET /posts?sort=created_at:desc
GET /users?sort=-created_at # Descending (- prefix)
GET /products?sort=price:asc,name:asc # Multi-field
Pagination:
GET /posts?page=2&limit=20 # Page-based
GET /posts?offset=20&limit=20 # Offset-based
GET /posts?cursor=abc123&limit=20 # Cursor-based (best for large datasets)
Field selection (sparse fieldsets):
GET /users?fields=id,email,name # Only return specified fields
GET /posts?include=author,comments # Include related resources
Use verbs for actions (not CRUD):
POST /posts/:id/publish # Publish post
POST /users/:id/send-reset # Send password reset
POST /orders/:id/cancel # Cancel order
POST /carts/:id/checkout # Checkout cart
Alternative: Use status changes:
PATCH /posts/:id # { "status": "published" }
PATCH /orders/:id # { "status": "cancelled" }
Safe + Idempotent: GET, HEAD, OPTIONS Idempotent (not safe): PUT, DELETE
GET: Multiple requests return same data
GET /users/42 # Always returns same user (if unchanged)
PUT: Multiple identical requests = same result
PUT /users/42
{"name": "Alice"} # Setting name to "Alice" multiple times = same result
DELETE: Multiple deletes = same result
DELETE /users/42 # First: 204 No Content
DELETE /users/42 # Second: 404 Not Found (idempotent result)
POST: Multiple requests create multiple resources
POST /orders # Creates new order each time
POST /payments # Processes payment each time (dangerous!)
Making POST idempotent (idempotency keys):
POST /payments
Idempotency-Key: abc123
{"amount": 100}
# Second request with same key returns original response (no duplicate charge)
POST /payments
Idempotency-Key: abc123
{"amount": 100} # Returns 200 OK with original payment (no new charge)
Implementation:
def create_payment(request):
idempotency_key = request.headers.get('Idempotency-Key')
# Check if already processed
existing = db.query("SELECT * FROM payments WHERE idempotency_key = %s", [idempotency_key])
if existing:
return 200, existing # Return original result
# Process new payment
payment = process_payment(request.body)
payment.idempotency_key = idempotency_key
db.save(payment)
return 201, payment
Single endpoint, single method:
POST /api
{"method": "getUser", "id": 42}
{"method": "createUser", "data": {...}}
❌ Not RESTful
Multiple endpoints (resources):
GET /users/42
POST /users
✅ Resource-based, but not using HTTP methods correctly
Proper use of HTTP methods:
GET /users/42 # Retrieve
POST /users # Create
PUT /users/42 # Update
DELETE /users/42 # Delete
✅ RESTful, uses HTTP semantics
Hypermedia As The Engine Of Application State:
Responses include links to related resources:
GET /users/42
{
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"_links": {
"self": {"href": "/users/42"},
"posts": {"href": "/users/42/posts"},
"followers": {"href": "/users/42/followers"},
"edit": {"href": "/users/42", "method": "PUT"},
"delete": {"href": "/users/42", "method": "DELETE"}
}
}
Benefits:
Trade-offs:
Client specifies format:
POST /users
Content-Type: application/json
{"name": "Alice"}
Server validates:
415 Unsupported Media Type (if Content-Type not supported)
Client requests format:
GET /users/42
Accept: application/json # Request JSON
Server responds:
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 42, "name": "Alice"}
Multiple formats supported:
GET /users/42
Accept: application/xml # Request XML
HTTP/1.1 200 OK
Content-Type: application/xml
<user><id>42</id><name>Alice</name></user>
Versioning via content type:
Accept: application/vnd.myapi.v2+json
GET /v1/users/42
GET /v2/users/42
Pros: Simple, explicit, easy to route Cons: URL pollution, hard to migrate
GET /users/42
Accept: application/vnd.myapi.v2+json
Pros: Clean URLs, RESTful Cons: Harder to test (can't use browser directly)
GET /users/42?version=2
Pros: Simple, easy to test Cons: Less RESTful, query params should be for filtering
Recommendation: URL versioning (/v1/, /v2/) for simplicity
| Code | Name | Use Case | |------|------|----------| | 200 | OK | Successful GET, PUT, PATCH | | 201 | Created | Successful POST (resource created) | | 204 | No Content | Successful DELETE or PUT with no response | | 400 | Bad Request | Validation error, malformed request | | 401 | Unauthorized | Authentication required or failed | | 403 | Forbidden | Authenticated but not authorized | | 404 | Not Found | Resource doesn't exist | | 409 | Conflict | Resource already exists, version conflict | | 422 | Unprocessable Entity | Valid syntax, semantic error | | 429 | Too Many Requests | Rate limit exceeded | | 500 | Internal Server Error | Unexpected server error | | 502 | Bad Gateway | Upstream service error | | 503 | Service Unavailable | Server under maintenance |
Resource Modeling:
[ ] Resources are nouns (not verbs)
[ ] Plural nouns for collections (/users, /posts)
[ ] Kebab-case for multi-word resources (/user-profiles)
[ ] Hierarchical relationships (/users/:id/posts)
HTTP Methods:
[ ] GET for retrieval (safe, idempotent)
[ ] POST for creation (non-idempotent)
[ ] PUT for full replacement (idempotent)
[ ] PATCH for partial updates
[ ] DELETE for removal (idempotent)
Status Codes:
[ ] 200 for successful GET/PUT/PATCH
[ ] 201 for successful POST (with Location header)
[ ] 204 for successful DELETE
[ ] 400 for validation errors
[ ] 401 for authentication errors
[ ] 403 for authorization errors
[ ] 404 for not found
[ ] 500 for server errors
Idempotency:
[ ] GET, PUT, DELETE are idempotent
[ ] POST uses idempotency keys for critical operations
[ ] PATCH operations are designed to be repeatable
Response Format:
[ ] Consistent error structure
[ ] Pagination metadata for collections
[ ] Timestamps in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)
[ ] Snake_case or camelCase (consistent across API)
Security:
[ ] Authentication required for sensitive endpoints
[ ] Authorization checks before operations
[ ] Rate limiting implemented
[ ] Input validation on all endpoints
❌ Verbs in URLs: /getUser, /createPost ✅ Use nouns + HTTP methods: GET /users, POST /posts
❌ Using GET for actions: GET /users/42/delete ✅ Use DELETE: DELETE /users/42
❌ Returning 200 for errors: 200 OK {"error": "User not found"} ✅ Use proper status codes: 404 Not Found {"error": "User not found"}
❌ Inconsistent naming: /users, /post, /getComments ✅ Consistent plural nouns: /users, /posts, /comments
❌ Deep nesting: /orgs/:id/teams/:id/users/:id/posts/:id/comments/:id ✅ Limit to 2-3 levels: /posts/:id/comments or /comments?post_id=:id
❌ Ignoring idempotency: POST /payments (no idempotency key) ✅ Implement idempotency: Idempotency-Key header
❌ Exposing internal IDs: /users/42 (auto-increment) ✅ Use UUIDs or obfuscated IDs: /users/550e8400-e29b-41d4-a716-446655440000
http-caching-strategies.md - Cache headers, ETags, conditional requestsapi-authentication.md - JWT, OAuth2, API keysapi-rate-limiting.md - Rate limiting strategiesgraphql-vs-rest.md - When to use GraphQL vs RESTopenapi-documentation.md - Documenting APIs with OpenAPI/SwaggerThis skill includes comprehensive Level 3 resources in the /resources/ directory:
resources/REFERENCE.md)Comprehensive 1,800+ line reference covering:
resources/scripts/)validate_api.py:
generate_openapi.py:
test_api.sh:
resources/examples/)python/fastapi_rest.py:
node/express_rest.js:
openapi/petstore.yaml:
typescript/api_client.ts:
curl/test_requests.sh:
# Validate API design
./resources/scripts/validate_api.py openapi.json --type spec --json
# Generate OpenAPI from code
./resources/scripts/generate_openapi.py from-code api.py --framework fastapi -o openapi.json
# Test API endpoints
./resources/scripts/test_api.sh --url https://api.example.com --api-key TOKEN --verbose
# Run curl examples
cd resources/examples/curl && ./test_requests.sh
# Run FastAPI example
cd resources/examples/python && python fastapi_rest.py
# Run Express example
cd resources/examples/node && node express_rest.js
Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)