Get the FREE Ultimate OpenClaw Setup Guide →

api-design-principles

npx machina-cli add skill wpank/ai/api-design-principles --openclaw
Files (1)
SKILL.md
10.0 KB

API Design Principles

WHAT

Design intuitive, scalable REST and GraphQL APIs that developers love. Covers resource modeling, HTTP semantics, pagination, error handling, versioning, and GraphQL schema patterns.

WHEN

  • Designing new REST or GraphQL APIs
  • Reviewing API specifications before implementation
  • Establishing API design standards for teams
  • Refactoring APIs for better usability
  • Migrating between API paradigms

KEYWORDS

REST, GraphQL, API design, HTTP methods, pagination, error handling, versioning, OpenAPI, HATEOAS, schema design

Installation

OpenClaw / Moltbot / Clawbot

npx clawhub@latest install api-design-principles

Decision Framework: REST vs GraphQL

Choose REST when...Choose GraphQL when...
Simple CRUD operationsComplex nested data requirements
Public APIs with broad audienceMobile apps needing bandwidth optimization
Heavy caching requirementsClients need to specify exact data shape
Team is unfamiliar with GraphQLAggregating multiple data sources
Simple response structuresRapidly evolving frontend requirements

REST API Design

Resource Naming Rules

✓ 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

HTTP Methods and Status Codes

MethodPurposeSuccessCommon Errors
GETRetrieve200 OK404 Not Found
POSTCreate201 Created400/422 Validation
PUTReplace200 OK404 Not Found
PATCHPartial update200 OK404 Not Found
DELETERemove204 No Content404/409 Conflict

Complete Status Code Reference

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
}

Pagination

Offset-Based (Simple)

GET /api/users?page=2&page_size=20

{
  "items": [...],
  "page": 2,
  "page_size": 20,
  "total": 150,
  "pages": 8
}

Cursor-Based (For Large Datasets)

GET /api/users?limit=20&cursor=eyJpZCI6MTIzfQ

{
  "items": [...],
  "next_cursor": "eyJpZCI6MTQzfQ",
  "has_more": true
}

Filtering and Sorting

# 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

Error Response Format

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"
  }
}

FastAPI Implementation

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)

GraphQL API Design

Schema Structure

# 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!
}

DataLoader (Prevent N+1)

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"])

Query Protection

# Depth limiting
MAX_QUERY_DEPTH = 5

# Complexity limiting
MAX_QUERY_COMPLEXITY = 100

# Timeout
QUERY_TIMEOUT_SECONDS = 10

Versioning Strategies

URL Versioning (Recommended)

/api/v1/users
/api/v2/users

Pros: Clear, easy to route, cacheable Cons: Multiple URLs for same resource

Header Versioning

GET /api/users
Accept: application/vnd.api+json; version=2

Pros: Clean URLs Cons: Less visible, harder to test

Deprecation Strategy

  1. Add deprecation headers: Deprecation: true
  2. Document migration path
  3. Give 6-12 months notice
  4. Monitor usage before removal

Rate Limiting

Headers

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1640000000

# When limited:
429 Too Many Requests
Retry-After: 3600

Implementation

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}

Pre-Implementation Checklist

Resources

  • Nouns, not verbs
  • Plural for collections
  • Max 2 levels nesting

HTTP

  • Correct method for each action
  • Correct status codes
  • Idempotent operations are idempotent

Data

  • All collections paginated
  • Filtering/sorting supported
  • Error format consistent

Security

  • Authentication defined
  • Rate limiting configured
  • Input validation on all fields
  • HTTPS enforced

Documentation

  • OpenAPI spec generated
  • All endpoints documented
  • Examples provided

NEVER

  • Verbs in URLs: /api/getUser → use /api/users/{id} with GET
  • POST for Retrieval: Use GET for safe, idempotent reads
  • Inconsistent Errors: Always same error format
  • Unbounded Lists: Always paginate collections
  • Secrets in URLs: Query params are logged
  • Breaking Changes Without Versioning: Plan for evolution from day 1
  • Database Schema as API: API should be stable even when schema changes
  • Ignoring HTTP Semantics: Status codes and methods have meaning

Source

git clone https://github.com/wpank/ai/blob/main/skills/backend/api-design-principles/SKILL.mdView on GitHub

Overview

Design intuitive, scalable REST and GraphQL APIs that developers love. This skill covers resource modeling, HTTP semantics, pagination, error handling, versioning, and GraphQL schema patterns to help teams build usable APIs.

How This Skill Works

Apply a practical decision framework to choose REST vs GraphQL; model resources with clear naming; implement robust pagination, consistent error responses, and versioning; and leverage GraphQL schemas when aggregating multiple data sources or shaping exact data needs.

When to Use It

  • Designing new REST or GraphQL APIs
  • Reviewing API specifications before implementation
  • Establishing API design standards for teams
  • Refactoring APIs for better usability
  • Migrating between API paradigms

Quick Start

  1. Step 1: Define resource models using plural nouns and shallow nesting (max 2 levels)
  2. Step 2: Choose REST vs GraphQL using the decision framework; plan pagination and error formats
  3. Step 3: Implement a consistent error response and a versioning strategy; create OpenAPI spec or GraphQL schema patterns

Best Practices

  • Name resources with plural nouns and prefer nested paths up to two levels; avoid verbs in paths
  • Use appropriate HTTP methods and standard status codes; differentiate 404, 409, 422, 429 as needed
  • Choose REST or GraphQL based on data needs and bandwidth considerations; leverage OpenAPI for REST and schema patterns for GraphQL
  • Implement consistent pagination (offset or cursor) and support filtering, sorting, and field selection
  • Standardize error responses with a consistent JSON structure and include timestamps and trace IDs; version APIs

Example Use Cases

  • REST resource naming: GET /api/users and GET /api/users/{id}/orders
  • Pagination example: offset-based GET /api/users?page=2&page_size=20 and cursor-based GET /api/users?limit=20&cursor=eyJpZCI6MTIzfQ
  • Error handling: a uniform error response with error.code, error.message, and details array
  • GraphQL pattern: schema with queries that allow clients to specify fields and join multiple data sources
  • Versioning: v1 endpoints under /api/v1 to enable non-breaking upgrades

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers