api-design-principlesDesign clear, scalable REST and GraphQL APIs including resource modeling, HTTP methods, pagination, error handling, versioning, and schema best practices.
Install via ClawdBot CLI:
clawdbot install wpank/api-design-principlesDesign intuitive, scalable REST and GraphQL APIs that developers love. Covers resource modeling, HTTP semantics, pagination, error handling, versioning, and GraphQL schema patterns.
REST, GraphQL, API design, HTTP methods, pagination, error handling, versioning, OpenAPI, HATEOAS, schema design
| Choose REST when... | Choose GraphQL when... |
|---------------------|------------------------|
| Simple CRUD operations | Complex nested data requirements |
| Public APIs with broad audience | Mobile apps needing bandwidth optimization |
| Heavy caching requirements | Clients need to specify exact data shape |
| Team is unfamiliar with GraphQL | Aggregating multiple data sources |
| Simple response structures | Rapidly evolving frontend requirements |
ā Plural nouns for collections
GET /api/users
GET /api/orders
GET /api/products
ā Avoid verbs (let HTTP methods be the verb)
POST /api/createUser ā Wrong
POST /api/users ā Correct
ā Nested resources (max 2 levels)
GET /api/users/{id}/orders
ā Avoid deep nesting
GET /api/users/{id}/orders/{orderId}/items/{itemId}/reviews ā Too deep
GET /api/order-items/{id}/reviews ā Better
| Method | Purpose | Success | Common Errors |
|--------|---------|---------|---------------|
| GET | Retrieve | 200 OK | 404 Not Found |
| POST | Create | 201 Created | 400/422 Validation |
| PUT | Replace | 200 OK | 404 Not Found |
| PATCH | Partial update | 200 OK | 404 Not Found |
| DELETE | Remove | 204 No Content | 404/409 Conflict |
SUCCESS = {
200: "OK", # GET, PUT, PATCH success
201: "Created", # POST success
204: "No Content", # DELETE success
}
CLIENT_ERROR = {
400: "Bad Request", # Malformed syntax
401: "Unauthorized", # Missing/invalid auth
403: "Forbidden", # Valid auth, no permission
404: "Not Found", # Resource doesn't exist
409: "Conflict", # State conflict (duplicate email)
422: "Unprocessable Entity", # Validation errors
429: "Too Many Requests", # Rate limited
}
SERVER_ERROR = {
500: "Internal Server Error",
503: "Service Unavailable", # Temporary downtime
}
GET /api/users?page=2&page_size=20
{
"items": [...],
"page": 2,
"page_size": 20,
"total": 150,
"pages": 8
}
GET /api/users?limit=20&cursor=eyJpZCI6MTIzfQ
{
"items": [...],
"next_cursor": "eyJpZCI6MTQzfQ",
"has_more": true
}
# Filtering
GET /api/users?status=active&role=admin
# Sorting (- prefix for descending)
GET /api/users?sort=-created_at,name
# Search
GET /api/users?search=john
# Field selection
GET /api/users?fields=id,name,email
Always use consistent structure:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{"field": "email", "message": "Invalid email format"}
],
"timestamp": "2025-10-16T12:00:00Z"
}
}
from fastapi import FastAPI, Query, Path, HTTPException, status
from pydantic import BaseModel, Field, EmailStr
from typing import Optional, List
from datetime import datetime
app = FastAPI(title="API", version="1.0.0")
# Models
class UserCreate(BaseModel):
email: EmailStr
name: str = Field(..., min_length=1, max_length=100)
class User(BaseModel):
id: str
email: str
name: str
created_at: datetime
class PaginatedResponse(BaseModel):
items: List[User]
total: int
page: int
page_size: int
pages: int
# Endpoints
@app.get("/api/users", response_model=PaginatedResponse)
async def list_users(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
status: Optional[str] = Query(None),
search: Optional[str] = Query(None)
):
"""List users with pagination and filtering."""
total = await count_users(status=status, search=search)
offset = (page - 1) * page_size
users = await fetch_users(limit=page_size, offset=offset, status=status, search=search)
return PaginatedResponse(
items=users,
total=total,
page=page,
page_size=page_size,
pages=(total + page_size - 1) // page_size
)
@app.post("/api/users", response_model=User, status_code=status.HTTP_201_CREATED)
async def create_user(user: UserCreate):
"""Create new user."""
if await user_exists(user.email):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={"code": "EMAIL_EXISTS", "message": "Email already registered"}
)
return await save_user(user)
@app.get("/api/users/{user_id}", response_model=User)
async def get_user(user_id: str = Path(...)):
"""Get user by ID."""
user = await fetch_user(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.delete("/api/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(user_id: str):
"""Delete user."""
if not await fetch_user(user_id):
raise HTTPException(status_code=404, detail="User not found")
await remove_user(user_id)
# Types
type User {
id: ID!
email: String!
name: String!
createdAt: DateTime!
orders(first: Int = 20, after: String): OrderConnection!
}
# Pagination (Relay-style)
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type OrderEdge {
node: Order!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Queries
type Query {
user(id: ID!): User
users(first: Int = 20, after: String, search: String): UserConnection!
}
# Mutations with Input/Payload pattern
input CreateUserInput {
email: String!
name: String!
password: String!
}
type CreateUserPayload {
user: User
errors: [Error!]
}
type Error {
field: String
message: String!
code: String!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
from aiodataloader import DataLoader
class UserLoader(DataLoader):
async def batch_load_fn(self, user_ids: List[str]) -> List[Optional[dict]]:
"""Load multiple users in single query."""
users = await fetch_users_by_ids(user_ids)
user_map = {user["id"]: user for user in users}
return [user_map.get(uid) for uid in user_ids]
# In resolver
@user_type.field("orders")
async def resolve_orders(user: dict, info):
loader = info.context["loaders"]["orders_by_user"]
return await loader.load(user["id"])
# Depth limiting
MAX_QUERY_DEPTH = 5
# Complexity limiting
MAX_QUERY_COMPLEXITY = 100
# Timeout
QUERY_TIMEOUT_SECONDS = 10
/api/v1/users
/api/v2/users
Pros: Clear, easy to route, cacheable
Cons: Multiple URLs for same resource
GET /api/users
Accept: application/vnd.api+json; version=2
Pros: Clean URLs
Cons: Less visible, harder to test
Deprecation: trueX-RateLimit-Limit: 1000
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1640000000
# When limited:
429 Too Many Requests
Retry-After: 3600
from datetime import datetime, timedelta
class RateLimiter:
def __init__(self, calls: int, period: int):
self.calls = calls
self.period = period
self.cache = {}
def check(self, key: str) -> tuple[bool, dict]:
now = datetime.now()
if key not in self.cache:
self.cache[key] = []
# Remove old requests
cutoff = now - timedelta(seconds=self.period)
self.cache[key] = [ts for ts in self.cache[key] if ts > cutoff]
remaining = self.calls - len(self.cache[key])
if remaining <= 0:
return False, {"limit": self.calls, "remaining": 0}
self.cache[key].append(now)
return True, {"limit": self.calls, "remaining": remaining - 1}
/api/getUser ā use /api/users/{id} with GETGenerated Mar 1, 2026
A retail company wants to redesign its legacy REST API to improve developer experience and support mobile apps. The API needs to handle product catalogs, user orders, and inventory management with efficient pagination and error handling for high traffic.
A healthcare startup is building a patient portal that aggregates data from multiple sources like EHR systems and lab results. GraphQL is chosen to allow clients to query nested data efficiently and reduce bandwidth for mobile users.
A bank is establishing API design standards for internal teams to ensure consistency across microservices. The focus is on REST principles for simple CRUD operations, secure authentication, and versioning to maintain backward compatibility.
A social media company is migrating from a REST API to GraphQL to handle complex nested relationships like user posts, comments, and likes. This supports rapidly evolving frontend requirements and optimizes data fetching for diverse client devices.
An IoT provider designs a REST API for managing connected devices, focusing on resource modeling for devices and sensors, with pagination for large datasets and error handling for device state conflicts.
Companies monetize their APIs by offering tiered access plans to external developers, charging based on usage metrics like request volume or data throughput. This model leverages well-designed APIs to generate recurring revenue from partners and third-party integrations.
Organizations build and maintain APIs for internal use across departments, improving operational efficiency and reducing development costs. Revenue is indirect, realized through cost savings and faster time-to-market for new features.
Firms offer expertise in API design principles, providing consulting services for API reviews, design workshops, and training programs. Revenue comes from project-based fees or ongoing retainer agreements with clients.
š¬ Integration Tip
Use OpenAPI specifications for REST APIs to generate documentation and client SDKs automatically, and implement GraphQL with tools like Apollo Server for efficient schema management and caching.
Use the mcporter CLI to list, configure, auth, and call MCP servers/tools directly (HTTP or stdio), including ad-hoc servers, config edits, and CLI/type generation.
Connect to 100+ APIs (Google Workspace, Microsoft 365, GitHub, Notion, Slack, Airtable, HubSpot, etc.) with managed OAuth. Use this skill when users want to...
Build, debug, and deploy websites using HTML, CSS, JavaScript, and modern frameworks following production best practices.
YouTube Data API integration with managed OAuth. Search videos, manage playlists, access channel data, and interact with comments. Use this skill when users want to interact with YouTube. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway).
Scaffold, test, document, and debug REST and GraphQL APIs. Use when the user needs to create API endpoints, write integration tests, generate OpenAPI specs, test with curl, mock APIs, or troubleshoot HTTP issues.
Search for jobs across LinkedIn, Indeed, Glassdoor, ZipRecruiter, Google Jobs, Bayt, Naukri, and BDJobs using the JobSpy MCP server.