ils15

api-design-patterns

Design RESTful APIs with proper HTTP methods, status codes, pagination, filtering, error responses, and OpenAPI documentation. Follows FastAPI best practices with Pydantic validation.

ils15 9 4 Updated 3mo ago
GitHub

Install

npx skillscat add ils15/mythic-agents/api-design-patterns

Install via the SkillsCat registry.

SKILL.md

API Design Patterns Skill

When to Use

Use this skill when:

  • Designing new API endpoints
  • Reviewing API consistency
  • Implementing pagination and filtering
  • Creating error response formats
  • Writing OpenAPI documentation
  • Validating HTTP status codes
  • Building request/response schemas

RESTful Design Principles

1. HTTP Methods

GET    /users          - List users
GET    /users/{id}     - Get single user
POST   /users          - Create user
PUT    /users/{id}     - Full update
PATCH  /users/{id}     - Partial update
DELETE /users/{id}     - Delete user

2. Status Codes

2xx Success:
- 200 OK              - Successful GET, PUT, PATCH
- 201 Created         - Successful POST
- 204 No Content      - Successful DELETE

4xx Client Errors:
- 400 Bad Request     - Invalid input
- 401 Unauthorized    - Not authenticated
- 403 Forbidden       - Not authorized
- 404 Not Found       - Resource not found
- 409 Conflict        - Duplicate resource
- 422 Unprocessable   - Validation error

5xx Server Errors:
- 500 Internal Error  - Unexpected error
- 503 Service Unavail - Maintenance

3. Pagination Pattern

from pydantic import BaseModel
from typing import Generic, TypeVar, List

T = TypeVar('T')

class PaginatedResponse(BaseModel, Generic[T]):
    items: List[T]
    total: int
    page: int
    size: int
    pages: int
    
    class Config:
        from_attributes = True

# Endpoint
@router.get("/users", response_model=PaginatedResponse[UserResponse])
async def list_users(
    page: int = Query(1, ge=1),
    size: int = Query(20, ge=1, le=100),
):
    total = await user_service.count()
    items = await user_service.get_page(page, size)
    return PaginatedResponse(
        items=items,
        total=total,
        page=page,
        size=size,
        pages=(total + size - 1) // size
    )

4. Filtering Pattern

from typing import Optional
from fastapi import Query

@router.get("/products")
async def list_products(
    # Search
    q: Optional[str] = Query(None, min_length=2, max_length=100),
    
    # Filters
    category: Optional[str] = None,
    min_price: Optional[float] = Query(None, ge=0),
    max_price: Optional[float] = Query(None, ge=0),
    in_stock: Optional[bool] = None,
    
    # Sorting
    sort_by: str = Query("created_at", regex="^(name|price|created_at)$"),
    order: str = Query("desc", regex="^(asc|desc)$"),
    
    # Pagination
    page: int = Query(1, ge=1),
    size: int = Query(20, ge=1, le=100),
):
    return await product_service.search(
        q=q, category=category, 
        min_price=min_price, max_price=max_price,
        in_stock=in_stock,
        sort_by=sort_by, order=order,
        page=page, size=size
    )

5. Error Response Format

from pydantic import BaseModel
from typing import Optional, List

class ErrorDetail(BaseModel):
    loc: List[str]
    msg: str
    type: str

class ErrorResponse(BaseModel):
    error: str
    message: str
    details: Optional[List[ErrorDetail]] = None
    request_id: Optional[str] = None

# Usage
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
    return JSONResponse(
        status_code=exc.status_code,
        content=ErrorResponse(
            error=exc.detail,
            message=str(exc.detail),
            request_id=request.state.request_id
        ).model_dump()
    )

6. Request/Response Schemas

from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
from typing import Optional

# Create (input)
class UserCreate(BaseModel):
    email: EmailStr
    name: str = Field(..., min_length=2, max_length=100)
    password: str = Field(..., min_length=8)

# Update (partial)
class UserUpdate(BaseModel):
    email: Optional[EmailStr] = None
    name: Optional[str] = Field(None, min_length=2, max_length=100)

# Response (output)
class UserResponse(BaseModel):
    id: int
    email: str
    name: str
    created_at: datetime
    
    class Config:
        from_attributes = True

# List response
class UserListResponse(BaseModel):
    id: int
    name: str  # Minimal fields for list

7. OpenAPI Documentation

@router.post(
    "/users",
    response_model=UserResponse,
    status_code=201,
    summary="Create a new user",
    description="Register a new user account with email and password.",
    responses={
        201: {"description": "User created successfully"},
        400: {"description": "Invalid input data"},
        409: {"description": "Email already exists"},
    },
    tags=["users"],
)
async def create_user(user: UserCreate):
    """
    Create a new user with the following information:
    
    - **email**: valid email address (unique)
    - **name**: user display name (2-100 chars)
    - **password**: secure password (min 8 chars)
    """
    return await user_service.create(user)

Output Format

## API Design Review

### Endpoints Analyzed
- GET /users - ✅ Correct
- POST /users - ⚠️ Missing 409 response
- DELETE /users/{id} - ❌ Returns 200 (should be 204)

### Issues Found
1. [Endpoint] - [Issue] - [Fix]

### Recommendations
1. Add pagination to list endpoints
2. Standardize error response format
3. Add request_id to all responses

Example Usage

@backend Design CRUD endpoints for products resource
@backend Review API consistency across all endpoints
@backend Add pagination to the orders list endpoint
@backend Create error response schema for the API