Claude Code Plugins

Community-maintained marketplace

Feedback

Design and review RESTful API endpoints following FastAPI best practices, OpenAPI standards, and consistent patterns. Use when adding new endpoints, modifying API contracts, or reviewing API design consistency.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name api-design
description Design and review RESTful API endpoints following FastAPI best practices, OpenAPI standards, and consistent patterns. Use when adding new endpoints, modifying API contracts, or reviewing API design consistency.
allowed-tools Read, Grep, Glob
model claude-sonnet-4-5-20250929

API Design for FastAPI

When This Skill Activates

You should use this skill when:

  • Designing new API endpoints
  • Modifying existing endpoint contracts
  • Reviewing API consistency across the project
  • Adding request/response models
  • Implementing error handling patterns
  • The user asks for API design guidance
  • Before documenting API changes

RESTful API Principles

1. Resource-Oriented Design

Good:

# ✅ Resource-based URLs
@app.get("/conversations/{conversation_id}")
@app.post("/conversations")
@app.delete("/conversations/{conversation_id}")

# ✅ Nested resources
@app.get("/conversations/{conversation_id}/messages")
@app.post("/conversations/{conversation_id}/messages")

Bad:

# ❌ Action-based URLs
@app.post("/createConversation")
@app.post("/deleteConversation")
@app.get("/getMessages")

2. HTTP Method Semantics

Method Use Case Idempotent Safe
GET Retrieve resource(s)
POST Create resource, non-idempotent actions
PUT Replace entire resource
PATCH Partially update resource
DELETE Delete resource

Good:

@app.get("/users/{user_id}")        # ✅ Retrieve
async def get_user(user_id: int): ...

@app.post("/users")                 # ✅ Create
async def create_user(user: UserCreate): ...

@app.put("/users/{user_id}")        # ✅ Full replace
async def replace_user(user_id: int, user: UserUpdate): ...

@app.patch("/users/{user_id}")      # ✅ Partial update
async def update_user(user_id: int, updates: UserPartial): ...

@app.delete("/users/{user_id}")     # ✅ Delete
async def delete_user(user_id: int): ...

3. Status Code Usage

Code Meaning Use When
200 OK Successful GET, PUT, PATCH, DELETE
201 Created Successful POST creating a resource
204 No Content Successful DELETE with no response body
400 Bad Request Invalid request data
401 Unauthorized Missing or invalid authentication
403 Forbidden Authenticated but not authorized
404 Not Found Resource doesn't exist
422 Unprocessable Entity Validation errors (FastAPI default)
500 Internal Server Error Unexpected server error

Good:

from fastapi import HTTPException, status

@app.post("/users", status_code=status.HTTP_201_CREATED)  # ✅ 201 for creation
async def create_user(user: UserCreate):
    new_user = await db.create_user(user)
    return new_user

@app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)  # ✅ 204 for delete
async def delete_user(user_id: int):
    await db.delete_user(user_id)
    return

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await db.get_user(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")  # ✅ 404
    return user

FastAPI Request/Response Patterns

1. Pydantic Models for Type Safety

Always use Pydantic models, not dicts:

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

# Request models
class UserCreate(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr
    password: str = Field(..., min_length=12)

class UserUpdate(BaseModel):
    username: Optional[str] = Field(None, min_length=3, max_length=50)
    email: Optional[EmailStr] = None

# Response models
class UserResponse(BaseModel):
    id: int
    username: str
    email: EmailStr
    created_at: datetime

    class Config:
        from_attributes = True  # For SQLAlchemy models

Usage:

@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate) -> UserResponse:
    # ✅ Type-safe request and response
    new_user = await db.create_user(user)
    return new_user

2. Separate Models for Different Operations

Pattern:

  • *Create - For POST requests (required fields)
  • *Update - For PUT requests (all fields, replaces entire resource)
  • *Partial - For PATCH requests (all fields optional)
  • *Response - For responses (includes computed/server fields)

Example:

class ConversationCreate(BaseModel):
    title: str
    context_id: int

class ConversationUpdate(BaseModel):
    title: str
    context_id: int
    metadata: dict

class ConversationPartial(BaseModel):
    title: Optional[str] = None
    metadata: Optional[dict] = None

class ConversationResponse(BaseModel):
    id: int
    title: str
    context_id: int
    metadata: dict
    created_at: datetime
    updated_at: datetime
    message_count: int  # Computed field

    class Config:
        from_attributes = True

3. Pagination Pattern

Consistent pagination across all list endpoints:

from typing import Generic, TypeVar
from pydantic import BaseModel

T = TypeVar('T')

class PaginatedResponse(BaseModel, Generic[T]):
    items: list[T]
    total: int
    page: int
    page_size: int
    has_next: bool

@app.get("/conversations", response_model=PaginatedResponse[ConversationResponse])
async def list_conversations(
    page: int = Query(1, ge=1),
    page_size: int = Query(50, ge=1, le=100)
):
    offset = (page - 1) * page_size

    # Get total count
    total = await db.count_conversations()

    # Get paginated items
    conversations = await db.get_conversations(limit=page_size, offset=offset)

    return PaginatedResponse(
        items=conversations,
        total=total,
        page=page,
        page_size=page_size,
        has_next=offset + page_size < total
    )

4. Filtering and Sorting

Use query parameters for filtering:

from typing import Optional
from enum import Enum

class SortOrder(str, Enum):
    asc = "asc"
    desc = "desc"

@app.get("/conversations")
async def list_conversations(
    context_id: Optional[int] = None,  # ✅ Filter by context
    search: Optional[str] = None,      # ✅ Text search
    sort_by: str = "created_at",       # ✅ Sort field
    sort_order: SortOrder = SortOrder.desc,  # ✅ Sort direction
    page: int = 1,
    page_size: int = 50
):
    filters = {}
    if context_id:
        filters["context_id"] = context_id
    if search:
        filters["title__ilike"] = f"%{search}%"

    conversations = await db.get_conversations(
        filters=filters,
        sort_by=sort_by,
        sort_order=sort_order.value,
        limit=page_size,
        offset=(page - 1) * page_size
    )

    return conversations

Error Handling Patterns

1. Consistent Error Response Format

Define standard error model:

from pydantic import BaseModel

class ErrorDetail(BaseModel):
    field: Optional[str] = None
    message: str
    code: Optional[str] = None

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

2. Custom Exception Handlers

Register global handlers:

from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
    return JSONResponse(
        status_code=400,
        content=ErrorResponse(
            error="Invalid value",
            details=[ErrorDetail(message=str(exc))]
        ).dict()
    )

@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content=ErrorResponse(
            error=exc.detail,
            request_id=request.state.request_id if hasattr(request.state, "request_id") else None
        ).dict()
    )

3. Validation Error Responses

FastAPI automatically formats Pydantic validation errors:

# Request with invalid data:
# POST /users
# {"username": "ab", "email": "invalid"}

# Response (422 Unprocessable Entity):
{
  "detail": [
    {
      "loc": ["body", "username"],
      "msg": "ensure this value has at least 3 characters",
      "type": "value_error.any_str.min_length"
    },
    {
      "loc": ["body", "email"],
      "msg": "value is not a valid email address",
      "type": "value_error.email"
    }
  ]
}

Dependency Injection for Common Logic

1. Authentication Dependencies

from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

security = HTTPBearer()

async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security)
) -> User:
    token = credentials.credentials
    user = await verify_token(token)
    if not user:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

async def require_admin(user: User = Depends(get_current_user)) -> User:
    if not user.is_admin:
        raise HTTPException(status_code=403, detail="Admin access required")
    return user

# Usage
@app.get("/users")
async def list_users(current_user: User = Depends(get_current_user)):
    # current_user is automatically injected
    ...

@app.delete("/users/{user_id}")
async def delete_user(
    user_id: int,
    admin: User = Depends(require_admin)  # ✅ Requires admin
):
    ...

2. Database Session Dependencies

from core.db import get_session
from sqlalchemy.ext.asyncio import AsyncSession

@app.get("/conversations/{conversation_id}")
async def get_conversation(
    conversation_id: int,
    session: AsyncSession = Depends(get_session)  # ✅ Auto-managed session
):
    stmt = select(Conversation).where(Conversation.id == conversation_id)
    result = await session.execute(stmt)
    conversation = result.scalar_one_or_none()

    if not conversation:
        raise HTTPException(status_code=404)

    return conversation

3. Common Query Parameters

from typing import Annotated

class PaginationParams(BaseModel):
    page: int = Query(1, ge=1)
    page_size: int = Query(50, ge=1, le=100)

PaginationDep = Annotated[PaginationParams, Depends()]

@app.get("/conversations")
async def list_conversations(pagination: PaginationDep):
    # ✅ Reusable pagination
    offset = (pagination.page - 1) * pagination.page_size
    ...

OpenAPI Documentation Best Practices

1. Descriptive Endpoint Documentation

@app.post(
    "/conversations",
    response_model=ConversationResponse,
    status_code=201,
    summary="Create a new conversation",
    description="Creates a new conversation within the specified context. "
                "Returns the created conversation with a unique ID.",
    responses={
        201: {"description": "Conversation created successfully"},
        400: {"description": "Invalid input data"},
        401: {"description": "Authentication required"},
        404: {"description": "Context not found"}
    },
    tags=["Conversations"]
)
async def create_conversation(
    conversation: ConversationCreate,
    current_user: User = Depends(get_current_user)
) -> ConversationResponse:
    """
    Create a new conversation.

    - **title**: Conversation title (required)
    - **context_id**: ID of the parent context (required)
    """
    ...

2. Request/Response Examples

class ConversationCreate(BaseModel):
    title: str
    context_id: int

    class Config:
        json_schema_extra = {
            "example": {
                "title": "Project Planning Discussion",
                "context_id": 42
            }
        }

3. Tags for Organization

# Group related endpoints
tags_metadata = [
    {
        "name": "Conversations",
        "description": "Manage conversations and sessions"
    },
    {
        "name": "Messages",
        "description": "Handle messages within conversations"
    },
    {
        "name": "Agent",
        "description": "Agent completion and tool execution"
    }
]

app = FastAPI(openapi_tags=tags_metadata)

@app.get("/conversations", tags=["Conversations"])
async def list_conversations(): ...

@app.post("/conversations/{id}/messages", tags=["Messages"])
async def create_message(): ...

Project-Specific API Patterns

1. OpenAI-Compatible Endpoints

This project exposes OpenAI-compatible endpoints:

@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
    # ✅ Follows OpenAI API specification
    return ChatCompletionResponse(
        id=f"chatcmpl-{uuid.uuid4()}",
        object="chat.completion",
        created=int(time.time()),
        model=request.model,
        choices=[
            Choice(
                index=0,
                message=Message(role="assistant", content=response),
                finish_reason="stop"
            )
        ]
    )

2. Metadata Pattern

Consistent metadata across resources:

class BaseResponse(BaseModel):
    id: int
    created_at: datetime
    updated_at: datetime
    metadata: dict = Field(default_factory=dict)

class ConversationResponse(BaseResponse):
    title: str
    context_id: int
    # Inherits id, created_at, updated_at, metadata

3. Streaming Responses

For long-running agent operations:

from fastapi.responses import StreamingResponse

@app.post("/v1/agent/stream")
async def agent_stream(request: AgentRequest):
    async def generate():
        async for chunk in agent.execute_stream(request.prompt):
            yield f"data: {json.dumps(chunk)}\n\n"

        yield "data: [DONE]\n\n"

    return StreamingResponse(
        generate(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive"
        }
    )

API Versioning

URL-Based Versioning (Current Project Pattern)

# ✅ Version in URL prefix
@app.post("/v1/agent")
async def agent_v1(): ...

@app.post("/v2/agent")  # New version with breaking changes
async def agent_v2(): ...

API Design Checklist

When adding or reviewing endpoints:

  • Uses appropriate HTTP method (GET/POST/PUT/PATCH/DELETE)
  • Resource-oriented URL structure (/resources/{id})
  • Correct status codes (200, 201, 204, 400, 401, 403, 404, 422, 500)
  • Request validated with Pydantic models
  • Response typed with response_model
  • Separate models for Create/Update/Partial/Response
  • Pagination for list endpoints
  • Filtering and sorting via query parameters
  • Authentication required where appropriate (via Depends)
  • Authorization checks for sensitive operations
  • Error responses follow consistent format
  • OpenAPI documentation complete (summary, description, examples)
  • Endpoint tagged appropriately
  • Breaking changes use new version (/v2/...)

Common API Design Issues

❌ Issue 1: Generic Endpoints

Bad:

@app.post("/api/data")  # ❌ What data? What operation?
async def handle_data(data: dict): ...

Good:

@app.post("/conversations")  # ✅ Clear resource
async def create_conversation(conversation: ConversationCreate): ...

❌ Issue 2: Inconsistent Naming

Bad:

@app.get("/getConversations")  # ❌ camelCase, verb in URL
@app.post("/create-message")   # ❌ kebab-case, verb in URL
@app.delete("/DeleteUser/{id}")  # ❌ PascalCase

Good:

@app.get("/conversations")      # ✅ plural noun, lowercase
@app.post("/messages")          # ✅ consistent pattern
@app.delete("/users/{id}")      # ✅ HTTP method conveys action

❌ Issue 3: Untyped Responses

Bad:

@app.get("/users/{id}")
async def get_user(id: int):
    return await db.get_user(id)  # ❌ No response model

Good:

@app.get("/users/{id}", response_model=UserResponse)
async def get_user(id: int) -> UserResponse:  # ✅ Type-safe
    return await db.get_user(id)

❌ Issue 4: Missing Error Handling

Bad:

@app.get("/users/{id}")
async def get_user(id: int):
    return await db.get_user(id)  # ❌ What if user doesn't exist?

Good:

@app.get("/users/{id}")
async def get_user(id: int):
    user = await db.get_user(id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")  # ✅
    return user

Testing API Design

Use FastAPI's TestClient:

from fastapi.testclient import TestClient

def test_create_conversation():
    client = TestClient(app)

    response = client.post(
        "/conversations",
        json={"title": "Test", "context_id": 1}
    )

    assert response.status_code == 201  # ✅ Correct status
    data = response.json()
    assert "id" in data  # ✅ Returns created resource
    assert data["title"] == "Test"

After running this skill:

  • Review endpoint URLs for RESTful compliance
  • Verify request/response models are properly typed
  • Check status codes are semantically correct
  • Ensure consistent patterns across the API
  • Update OpenAPI documentation
  • Suggest improvements for consistency