Expert REST API design patterns, resource modeling, HTTP semantics, pagination, filtering, and RESTful best practices. Use when designing REST endpoints, reviewing API specifications, establishing REST conventions, or implementing HTTP-based web services.
Designs RESTful APIs with resource-oriented architecture, HTTP semantics, pagination, and best practices.
npx claudepluginhub karchtho/my-claude-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Master REST API design principles to build intuitive, scalable, and developer-friendly APIs that stand the test of time.
REST APIs should be resource-oriented, not action-oriented:
Good patterns:
GET /api/users # List users
POST /api/users # Create user
GET /api/users/{id} # Get specific user
PUT /api/users/{id} # Replace user
PATCH /api/users/{id} # Update user fields
DELETE /api/users/{id} # Delete user
# Nested resources (shallow)
GET /api/users/{id}/orders # Get user's orders
POST /api/users/{id}/orders # Create order for user
Bad patterns (avoid):
POST /api/createUser
POST /api/getUserById
POST /api/deleteUser
GET /api/user (inconsistent singular)
Each HTTP method has specific semantics that must be respected:
Idempotency: GET, PUT, DELETE must be idempotent (same result when called multiple times)
Resource Naming:
/api/users not /api/user/api/users not /api/Users/api/user-profiles not /api/userProfilesNested Resources (Shallow Preferred):
# Shallow nesting (preferred) - easy to understand
GET /api/users/{id}/orders
# Deep nesting (avoid) - hard to route and query
GET /api/users/{id}/orders/{orderId}/items/{itemId}/reviews
# Better approach for deep hierarchies:
GET /api/order-items/{id}/reviews
Use status codes correctly to communicate request outcomes:
200 OK - Successful GET, PATCH, PUT201 Created - Successful POST (include Location header)204 No Content - Successful DELETE or empty response400 Bad Request - Malformed request syntax401 Unauthorized - Authentication required403 Forbidden - Authenticated but not authorized404 Not Found - Resource doesn't exist409 Conflict - State conflict (duplicate email, etc.)422 Unprocessable Entity - Validation errors500 Internal Server Error - Server error503 Service Unavailable - Temporary downtime429 Too Many Requests - Rate limit exceeded# List collections
GET /api/users?page=1&limit=20
→ 200 OK
→ Returns: [user1, user2, ...]
# Get specific resource
GET /api/users/{id}
→ 200 OK or 404 Not Found
→ Returns: {id, name, email, ...}
# Create new resource
POST /api/users
Body: {"name": "John", "email": "john@example.com"}
→ 201 Created
→ Location: /api/users/123
→ Returns: {id: "123", name: "John", ...}
# Update entire resource
PUT /api/users/{id}
Body: {complete user object with ALL fields}
→ 200 OK or 404 Not Found
# Partial update
PATCH /api/users/{id}
Body: {"name": "Jane"} (only changed fields)
→ 200 OK or 404 Not Found
# Delete resource
DELETE /api/users/{id}
→ 204 No Content or 404 Not Found
Always paginate large collections. Three main approaches:
Best for: Small datasets, traditional pagination UI
GET /api/users?page=2&page_size=20
Response:
{
"items": [...],
"page": 2,
"page_size": 20,
"total": 150,
"pages": 8,
"has_next": true,
"has_prev": true
}
Best for: Large datasets, real-time data, consistent results
GET /api/users?limit=20&cursor=eyJpZCI6MTIzfQ
Response:
{
"items": [...],
"next_cursor": "eyJpZCI6MTQzfQ",
"prev_cursor": "eyJpZCI6MTA3fQ",
"has_more": true
}
Most RESTful approach using HTTP Link header:
GET /api/users?page=2
Response Headers:
Link: <https://api.example.com/users?page=3>; rel="next",
<https://api.example.com/users?page=1>; rel="prev",
<https://api.example.com/users?page=1>; rel="first",
<https://api.example.com/users?page=8>; rel="last"
Implementation pattern (FastAPI):
from fastapi import FastAPI, Query
from typing import Optional
@app.get("/api/users")
async def list_users(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100)
):
offset = (page - 1) * page_size
total = await count_users()
users = await fetch_users(limit=page_size, offset=offset)
return {
"items": users,
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size
}
Filtering:
GET /api/users?status=active
GET /api/users?role=admin&status=active
Sorting:
GET /api/users?sort=created_at
GET /api/users?sort=-created_at # descending
GET /api/users?sort=name,created_at # multiple fields
Searching:
GET /api/users?search=john
GET /api/users?q=john
Field Selection (Sparse Fieldsets):
GET /api/users?fields=id,name,email
Standardize error responses for consistency:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format",
"value": "not-an-email"
}
],
"timestamp": "2025-10-16T12:00:00Z",
"path": "/api/users"
}
}
Plan for breaking changes from day one:
Clear and easy to route:
/api/v1/users
/api/v2/users
Clean URLs but less visible:
GET /api/users
Accept: application/vnd.api+json; version=2
Easy to test but easy to forget:
GET /api/users?version=2
Bearer Token (JWT):
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
401 Unauthorized - Missing/invalid token
403 Forbidden - Valid token, insufficient permissions
API Keys:
X-API-Key: your-api-key-here
Protect APIs from abuse:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1640000000
Response when limited:
429 Too Many Requests
Retry-After: 3600
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["*"],
)
For non-idempotent operations (POST), use idempotency keys:
POST /api/orders
Idempotency-Key: unique-key-123
If duplicate request: → 200 OK (return cached response)
POST /api/users/batch
{
"items": [
{"name": "User1", "email": "user1@example.com"},
{"name": "User2", "email": "user2@example.com"}
]
}
Response:
{
"results": [
{"id": "1", "status": "created"},
{"id": null, "status": "failed", "error": "Email already exists"}
]
}
Include links for related resources:
{
"id": "123",
"name": "John",
"email": "john@example.com",
"_links": {
"self": {"href": "/api/users/123"},
"orders": {"href": "/api/users/123/orders"},
"update": {"href": "/api/users/123", "method": "PATCH"},
"delete": {"href": "/api/users/123", "method": "DELETE"}
}
}
Cache Headers:
# Client caching
Cache-Control: public, max-age=3600
# No caching
Cache-Control: no-cache, no-store, must-revalidate
# Conditional requests
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
→ 304 Not Modified
Generate interactive API documentation:
from fastapi import FastAPI, Path
app = FastAPI(
title="My API",
description="API for managing users",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
@app.get(
"/api/users/{user_id}",
summary="Get user by ID",
response_description="User details",
tags=["Users"]
)
async def get_user(
user_id: str = Path(..., description="The user ID")
):
"""
Retrieve user by ID.
Returns full user profile including:
- Basic information
- Contact details
- Account status
"""
pass
/api/createUser (wrong)/users and /userFor detailed guidance, see:
references/http-methods-guide.md - Comprehensive HTTP method semanticsreferences/pagination-patterns.md - Comparison of pagination approachesreferences/rest-error-handling.md - Error handling standards