# API Design Expert

> Expert-level API design principles, REST, GraphQL, versioning, and API best practices. Use when the user mentions REST, GraphQL, API versioning, or API security, or when the task involves API Design Principles, API Styles, or Documentation.

- Skill: `personamanagmentlayer/api-design-expert` (Agent Skill)
- Install (CLI): `npx skillmds add personamanagmentlayer/api-design-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/personamanagmentlayer/api-design-expert/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: personamanagmentlayer (https://skillmd.com/u/personamanagmentlayer)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/personamanagmentlayer/api-design-expert

---


# API Design Expert

Expert guidance for API design, RESTful principles, GraphQL, versioning strategies, and API best practices.

## Core Concepts

### API Design Principles

- RESTful architecture
- Resource-oriented design
- Uniform interface
- Statelessness
- Cacheability
- Layered system

### API Styles

- REST (Representational State Transfer)
- GraphQL
- RPC (Remote Procedure Call)
- WebSocket
- Server-Sent Events (SSE)
- gRPC

### Key Considerations

- Versioning strategies
- Authentication and authorization
- Rate limiting
- Error handling
- Documentation
- Backward compatibility

## REST API Design

```python
from fastapi import FastAPI, HTTPException, Query, Path, Header
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
from enum import Enum

app = FastAPI(
    title="User Management API",
    version="1.0.0",
    description="RESTful API for user management"
)

# Models
class UserRole(str, Enum):
    ADMIN = "admin"
    USER = "user"
    GUEST = "guest"

class UserCreate(BaseModel):
    email: str = Field(..., example="user@example.com")
    name: str = Field(..., min_length=1, max_length=100)
    role: UserRole = UserRole.USER

class UserResponse(BaseModel):
    id: str
    email: str
    name: str
    role: UserRole
    created_at: datetime
    updated_at: datetime

    class Config:
        schema_extra = {
            "example": {
                "id": "123e4567-e89b-12d3-a456-426614174000",
                "email": "user@example.com",
                "name": "John Doe",
                "role": "user",
                "created_at": "2024-01-01T00:00:00Z",
                "updated_at": "2024-01-01T00:00:00Z"
            }
        }

class UserUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    role: Optional[UserRole] = None

# REST Endpoints following best practices
@app.get("/api/v1/users",
         response_model=List[UserResponse],
         summary="List all users",
         tags=["Users"])
async def list_users(
    page: int = Query(1, ge=1, description="Page number"),
    page_size: int = Query(20, ge=1, le=100, description="Items per page"),
    sort: str = Query("created_at", description="Sort field"),
    order: str = Query("desc", regex="^(asc|desc)$")
):
    """
    Retrieve a paginated list of users.

    - **page**: Page number (starts at 1)
    - **page_size**: Number of items per page (1-100)
    - **sort**: Field to sort by
    - **order**: Sort order (asc or desc)
    """
    # Implementation
    return []

@app.get("/api/v1/users/{user_id}",
         response_model=UserResponse,
         summary="Get user by ID",
         tags=["Users"])
async def get_user(
    user_id: str = Path(..., description="User ID")
):
    """Retrieve a specific user by ID."""
    # Implementation
    raise HTTPException(status_code=404, detail="User not found")

@app.post("/api/v1/users",
          response_model=UserResponse,
          status_code=201,
          summary="Create new user",
          tags=["Users"])
async def create_user(user: UserCreate):
    """Create a new user."""
    # Implementation
    return UserResponse(
        id="123e4567-e89b-12d3-a456-426614174000",
        email=user.email,
        name=user.name,
        role=user.role,
        created_at=datetime.now(),
        updated_at=datetime.now()
    )

@app.patch("/api/v1/users/{user_id}",
           response_model=UserResponse,
           summary="Update user",
           tags=["Users"])
async def update_user(
    user_id: str = Path(..., description="User ID"),
    user: UserUpdate = None
):
    """Partially update a user."""
    # Implementation
    pass

@app.delete("/api/v1/users/{user_id}",
            status_code=204,
            summary="Delete user",
            tags=["Users"])
async def delete_user(
    user_id: str = Path(..., description="User ID")
):
    """Delete a user."""
    # Implementation
    pass

# Nested resources
@app.get("/api/v1/users/{user_id}/posts",
         summary="Get user posts",
         tags=["Users", "Posts"])
async def get_user_posts(user_id: str):
    """Retrieve all posts for a specific user."""
    return []
```

## API Versioning

```python
from fastapi import APIRouter, Request

# URL Path Versioning (Recommended)
v1_router = APIRouter(prefix="/api/v1")
v2_router = APIRouter(prefix="/api/v2")

@v1_router.get("/users")
async def get_users_v1():
    """Version 1: Returns basic user info"""
    return [{"id": 1, "name": "John"}]

@v2_router.get("/users")
async def get_users_v2():
    """Version 2: Returns enhanced user info"""
    return [{"id": 1, "name": "John", "email": "john@example.com"}]

# Header Versioning
async def version_from_header(request: Request):
    version = request.headers.get("API-Version", "1")
    return version

@app.get("/api/users")
async def get_users(version: str = Depends(version_from_header)):
    if version == "2":
        return get_users_v2()
    return get_users_v1()

# Content Negotiation Versioning
@app.get("/api/users")
async def get_users_content_negotiation(
    accept: str = Header("application/vnd.api+json; version=1")
):
    if "version=2" in accept:
        return get_users_v2()
    return get_users_v1()
```

## Error Handling

```python
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel, ValidationError

class ErrorResponse(BaseModel):
    error: str
    message: str
    details: Optional[dict] = None
    timestamp: datetime
    path: str

@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content=ErrorResponse(
            error=exc.status_code,
            message=exc.detail,
            timestamp=datetime.now(),
            path=request.url.path
        ).dict()
    )

@app.exception_handler(ValidationError)
async def validation_exception_handler(request: Request, exc: ValidationError):
    return JSONResponse(
        status_code=422,
        content=ErrorResponse(
            error="validation_error",
            message="Request validation failed",
            details=exc.errors(),
            timestamp=datetime.now(),
            path=request.url.path
        ).dict()
    )

# Custom business logic errors
class BusinessError(Exception):
    def __init__(self, message: str, code: str):
        self.message = message
        self.code = code

@app.exception_handler(BusinessError)
async def business_error_handler(request: Request, exc: BusinessError):
    return JSONResponse(
        status_code=400,
        content=ErrorResponse(
            error=exc.code,
            message=exc.message,
            timestamp=datetime.now(),
            path=request.url.path
        ).dict()
    )
```

## Rate Limiting

```python
from fastapi import Request, HTTPException
from datetime import datetime, timedelta
import redis
from typing import Dict

class RateLimiter:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client

    async def check_rate_limit(self,
                               key: str,
                               max_requests: int,
                               window_seconds: int) -> Dict:
        """
        Token bucket algorithm for rate limiting
        """
        now = datetime.now().timestamp()
        window_key = f"rate_limit:{key}:{int(now // window_seconds)}"

        pipe = self.redis.pipeline()
        pipe.incr(window_key)
        pipe.expire(window_key, window_seconds)
        result = pipe.execute()

        request_count = result[0]

        if request_count > max_requests:
            reset_time = (int(now // window_seconds) + 1) * window_seconds
            raise HTTPException(
                status_code=429,
                detail="Rate limit exceeded",
                headers={
                    "X-RateLimit-Limit": str(max_requests),
                    "X-RateLimit-Remaining": "0",
                    "X-RateLimit-Reset": str(reset_time)
                }
            )

        return {
            "limit": max_requests,
            "remaining": max_requests - request_count,
            "reset": (int(now // window_seconds) + 1) * window_seconds
        }

# Middleware for rate limiting
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    limiter = RateLimiter(redis_client)

    # Get client identifier (IP, API key, etc.)
    client_id = request.client.host

    try:
        rate_info = await limiter.check_rate_limit(
            client_id,
            max_requests=100,
            window_seconds=60
        )

        response = await call_next(request)

        # Add rate limit headers
        response.headers["X-RateLimit-Limit"] = str(rate_info["limit"])
        response.headers["X-RateLimit-Remaining"] = str(rate_info["remaining"])
        response.headers["X-RateLimit-Reset"] = str(rate_info["reset"])

        return response
    except HTTPException as e:
        return JSONResponse(
            status_code=e.status_code,
            content={"error": e.detail},
            headers=e.headers
        )
```

## HATEOAS Implementation

```python
from typing import Dict, List

class HATEOASResponse(BaseModel):
    data: dict
    links: Dict[str, str]

def create_links(resource_id: str, resource_type: str) -> Dict[str, str]:
    """Create HATEOAS links for a resource"""
    return {
        "self": f"/api/v1/{resource_type}/{resource_id}",
        "update": f"/api/v1/{resource_type}/{resource_id}",
        "delete": f"/api/v1/{resource_type}/{resource_id}",
        "collection": f"/api/v1/{resource_type}"
    }

@app.get("/api/v1/users/{user_id}", response_model=HATEOASResponse)
async def get_user_with_links(user_id: str):
    user = get_user_from_db(user_id)

    return HATEOASResponse(
        data=user,
        links={
            "self": f"/api/v1/users/{user_id}",
            "posts": f"/api/v1/users/{user_id}/posts",
            "profile": f"/api/v1/users/{user_id}/profile",
            "update": f"/api/v1/users/{user_id}",
            "delete": f"/api/v1/users/{user_id}"
        }
    )
```

## API Security

```python
import os

from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from passlib.context import CryptContext

security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# The JWT signing key. A literal here means every deployment that copies
# this file shares a forgeable key - load it, and fail closed if absent.
SECRET_KEY = os.environ["JWT_SECRET_KEY"]
ALGORITHM = "HS256"

async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    """Verify JWT token"""
    try:
        payload = jwt.decode(
            credentials.credentials,
            SECRET_KEY,
            algorithms=[ALGORITHM]
        )
        return payload
    except JWTError:
        raise HTTPException(
            status_code=401,
            detail="Invalid authentication credentials"
        )

@app.get("/api/v1/protected")
async def protected_route(token_payload: dict = Depends(verify_token)):
    return {"message": "Access granted", "user": token_payload}

# API Key Authentication
async def verify_api_key(api_key: str = Header(...)):
    """Verify API key"""
    if api_key not in valid_api_keys:
        raise HTTPException(status_code=403, detail="Invalid API key")
    return api_key
```

## Best Practices

### Design

- Use nouns for resources, not verbs
- Use plural names for collections
- Use HTTP methods correctly (GET, POST, PUT, PATCH, DELETE)
- Return appropriate status codes
- Support filtering, sorting, and pagination
- Use consistent naming conventions
- Version APIs from the start

### Documentation

- Use OpenAPI/Swagger
- Provide example requests and responses
- Document error codes and messages
- Include authentication requirements
- Keep documentation up-to-date
- Provide SDKs when possible

### Performance

- Implement caching (ETags, Cache-Control)
- Support compression (gzip)
- Paginate large result sets
- Use async operations for long tasks
- Implement rate limiting
- Monitor API performance

### Security

- Use HTTPS always
- Implement authentication and authorization
- Validate all inputs
- Rate limit to prevent abuse
- Log security events
- Use API keys or OAuth 2.0
- Implement CORS properly

## Anti-Patterns

❌ Using GET for state-changing operations
❌ Returning inconsistent response formats
❌ No versioning strategy
❌ Poor error messages
❌ No rate limiting
❌ Exposing internal implementation details
❌ Breaking changes without versioning

## Resources

- REST API Design Best Practices: https://restfulapi.net/
- OpenAPI Specification: https://swagger.io/specification/
- API Design Guide: https://cloud.google.com/apis/design
- Microsoft REST API Guidelines: https://github.com/microsoft/api-guidelines
- FastAPI: https://fastapi.tiangolo.com/

