Claude Agent Skill · by Wshobson

Api Design Principles

Solid coverage of both REST and GraphQL design patterns with practical examples you can actually use. The skill walks through resource-oriented URL design, prop

Install
Terminal · npx
$npx skills add https://github.com/wshobson/agents --skill api-design-principles
Works with Paperclip

How Api Design Principles fits into a Paperclip company.

Api Design Principles drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md518 lines
Expand
---name: api-design-principlesdescription: Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers. Use when designing new APIs, reviewing API specifications, or establishing API design standards.--- # API Design Principles Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers and stand the test of time. ## When to Use This Skill - Designing new REST or GraphQL APIs- Refactoring existing APIs for better usability- Establishing API design standards for your team- Reviewing API specifications before implementation- Migrating between API paradigms (REST to GraphQL, etc.)- Creating developer-friendly API documentation- Optimizing APIs for specific use cases (mobile, third-party integrations) ## Core Concepts ### 1. RESTful Design Principles **Resource-Oriented Architecture** - Resources are nouns (users, orders, products), not verbs- Use HTTP methods for actions (GET, POST, PUT, PATCH, DELETE)- URLs represent resource hierarchies- Consistent naming conventions **HTTP Methods Semantics:** - `GET`: Retrieve resources (idempotent, safe)- `POST`: Create new resources- `PUT`: Replace entire resource (idempotent)- `PATCH`: Partial resource updates- `DELETE`: Remove resources (idempotent) ### 2. GraphQL Design Principles **Schema-First Development** - Types define your domain model- Queries for reading data- Mutations for modifying data- Subscriptions for real-time updates **Query Structure:** - Clients request exactly what they need- Single endpoint, multiple operations- Strongly typed schema- Introspection built-in ### 3. API Versioning Strategies **URL Versioning:** ```/api/v1/users/api/v2/users``` **Header Versioning:** ```Accept: application/vnd.api+json; version=1``` **Query Parameter Versioning:** ```/api/users?version=1``` ## REST API Design Patterns ### Pattern 1: Resource Collection Design ```python# Good: Resource-oriented endpointsGET    /api/users              # List users (with pagination)POST   /api/users              # Create userGET    /api/users/{id}         # Get specific userPUT    /api/users/{id}         # Replace userPATCH  /api/users/{id}         # Update user fieldsDELETE /api/users/{id}         # Delete user # Nested resourcesGET    /api/users/{id}/orders  # Get user's ordersPOST   /api/users/{id}/orders  # Create order for user # Bad: Action-oriented endpoints (avoid)POST   /api/createUserPOST   /api/getUserByIdPOST   /api/deleteUser``` ### Pattern 2: Pagination and Filtering ```pythonfrom typing import List, Optionalfrom pydantic import BaseModel, Field class PaginationParams(BaseModel):    page: int = Field(1, ge=1, description="Page number")    page_size: int = Field(20, ge=1, le=100, description="Items per page") class FilterParams(BaseModel):    status: Optional[str] = None    created_after: Optional[str] = None    search: Optional[str] = None class PaginatedResponse(BaseModel):    items: List[dict]    total: int    page: int    page_size: int    pages: int     @property    def has_next(self) -> bool:        return self.page < self.pages     @property    def has_prev(self) -> bool:        return self.page > 1 # FastAPI endpoint examplefrom fastapi import FastAPI, Query, Depends app = FastAPI() @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)):    # Apply filters    query = build_query(status=status, search=search)     # Count total    total = await count_users(query)     # Fetch page    offset = (page - 1) * page_size    users = await fetch_users(query, limit=page_size, offset=offset)     return PaginatedResponse(        items=users,        total=total,        page=page,        page_size=page_size,        pages=(total + page_size - 1) // page_size    )``` ### Pattern 3: Error Handling and Status Codes ```pythonfrom fastapi import HTTPException, statusfrom pydantic import BaseModel class ErrorResponse(BaseModel):    error: str    message: str    details: Optional[dict] = None    timestamp: str    path: str class ValidationErrorDetail(BaseModel):    field: str    message: str    value: Any # Consistent error responsesSTATUS_CODES = {    "success": 200,    "created": 201,    "no_content": 204,    "bad_request": 400,    "unauthorized": 401,    "forbidden": 403,    "not_found": 404,    "conflict": 409,    "unprocessable": 422,    "internal_error": 500} def raise_not_found(resource: str, id: str):    raise HTTPException(        status_code=status.HTTP_404_NOT_FOUND,        detail={            "error": "NotFound",            "message": f"{resource} not found",            "details": {"id": id}        }    ) def raise_validation_error(errors: List[ValidationErrorDetail]):    raise HTTPException(        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,        detail={            "error": "ValidationError",            "message": "Request validation failed",            "details": {"errors": [e.dict() for e in errors]}        }    ) # Example usage@app.get("/api/users/{user_id}")async def get_user(user_id: str):    user = await fetch_user(user_id)    if not user:        raise_not_found("User", user_id)    return user``` ### Pattern 4: HATEOAS (Hypermedia as the Engine of Application State) ```pythonclass UserResponse(BaseModel):    id: str    name: str    email: str    _links: dict     @classmethod    def from_user(cls, user: User, base_url: str):        return cls(            id=user.id,            name=user.name,            email=user.email,            _links={                "self": {"href": f"{base_url}/api/users/{user.id}"},                "orders": {"href": f"{base_url}/api/users/{user.id}/orders"},                "update": {                    "href": f"{base_url}/api/users/{user.id}",                    "method": "PATCH"                },                "delete": {                    "href": f"{base_url}/api/users/{user.id}",                    "method": "DELETE"                }            }        )``` ## GraphQL Design Patterns ### Pattern 1: Schema Design ```graphql# schema.graphql # Clear type definitionstype User {  id: ID!  email: String!  name: String!  createdAt: DateTime!   # Relationships  orders(first: Int = 20, after: String, status: OrderStatus): OrderConnection!   profile: UserProfile} type Order {  id: ID!  status: OrderStatus!  total: Money!  items: [OrderItem!]!  createdAt: DateTime!   # Back-reference  user: User!} # Pagination pattern (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} # Enums for type safetyenum OrderStatus {  PENDING  CONFIRMED  SHIPPED  DELIVERED  CANCELLED} # Custom scalarsscalar DateTimescalar Money # Query roottype Query {  user(id: ID!): User  users(first: Int = 20, after: String, search: String): UserConnection!   order(id: ID!): Order} # Mutation roottype Mutation {  createUser(input: CreateUserInput!): CreateUserPayload!  updateUser(input: UpdateUserInput!): UpdateUserPayload!  deleteUser(id: ID!): DeleteUserPayload!   createOrder(input: CreateOrderInput!): CreateOrderPayload!} # Input types for mutationsinput CreateUserInput {  email: String!  name: String!  password: String!} # Payload types for mutationstype CreateUserPayload {  user: User  errors: [Error!]} type Error {  field: String  message: String!}``` ### Pattern 2: Resolver Design ```pythonfrom typing import Optional, Listfrom ariadne import QueryType, MutationType, ObjectTypefrom dataclasses import dataclass query = QueryType()mutation = MutationType()user_type = ObjectType("User") @query.field("user")async def resolve_user(obj, info, id: str) -> Optional[dict]:    """Resolve single user by ID."""    return await fetch_user_by_id(id) @query.field("users")async def resolve_users(    obj,    info,    first: int = 20,    after: Optional[str] = None,    search: Optional[str] = None) -> dict:    """Resolve paginated user list."""    # Decode cursor    offset = decode_cursor(after) if after else 0     # Fetch users    users = await fetch_users(        limit=first + 1,  # Fetch one extra to check hasNextPage        offset=offset,        search=search    )     # Pagination    has_next = len(users) > first    if has_next:        users = users[:first]     edges = [        {            "node": user,            "cursor": encode_cursor(offset + i)        }        for i, user in enumerate(users)    ]     return {        "edges": edges,        "pageInfo": {            "hasNextPage": has_next,            "hasPreviousPage": offset > 0,            "startCursor": edges[0]["cursor"] if edges else None,            "endCursor": edges[-1]["cursor"] if edges else None        },        "totalCount": await count_users(search=search)    } @user_type.field("orders")async def resolve_user_orders(user: dict, info, first: int = 20) -> dict:    """Resolve user's orders (N+1 prevention with DataLoader)."""    # Use DataLoader to batch requests    loader = info.context["loaders"]["orders_by_user"]    orders = await loader.load(user["id"])     return paginate_orders(orders, first) @mutation.field("createUser")async def resolve_create_user(obj, info, input: dict) -> dict:    """Create new user."""    try:        # Validate input        validate_user_input(input)         # Create user        user = await create_user(            email=input["email"],            name=input["name"],            password=hash_password(input["password"])        )         return {            "user": user,            "errors": []        }    except ValidationError as e:        return {            "user": None,            "errors": [{"field": e.field, "message": e.message}]        }``` ### Pattern 3: DataLoader (N+1 Problem Prevention) ```pythonfrom aiodataloader import DataLoaderfrom typing import List, Optional class UserLoader(DataLoader):    """Batch load users by ID."""     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)         # Map results back to input order        user_map = {user["id"]: user for user in users}        return [user_map.get(user_id) for user_id in user_ids] class OrdersByUserLoader(DataLoader):    """Batch load orders by user ID."""     async def batch_load_fn(self, user_ids: List[str]) -> List[List[dict]]:        """Load orders for multiple users in single query."""        orders = await fetch_orders_by_user_ids(user_ids)         # Group orders by user_id        orders_by_user = {}        for order in orders:            user_id = order["user_id"]            if user_id not in orders_by_user:                orders_by_user[user_id] = []            orders_by_user[user_id].append(order)         # Return in input order        return [orders_by_user.get(user_id, []) for user_id in user_ids] # Context setupdef create_context():    return {        "loaders": {            "user": UserLoader(),            "orders_by_user": OrdersByUserLoader()        }    }``` ## Best Practices ### REST APIs 1. **Consistent Naming**: Use plural nouns for collections (`/users`, not `/user`)2. **Stateless**: Each request contains all necessary information3. **Use HTTP Status Codes Correctly**: 2xx success, 4xx client errors, 5xx server errors4. **Version Your API**: Plan for breaking changes from day one5. **Pagination**: Always paginate large collections6. **Rate Limiting**: Protect your API with rate limits7. **Documentation**: Use OpenAPI/Swagger for interactive docs ### GraphQL APIs 1. **Schema First**: Design schema before writing resolvers2. **Avoid N+1**: Use DataLoaders for efficient data fetching3. **Input Validation**: Validate at schema and resolver levels4. **Error Handling**: Return structured errors in mutation payloads5. **Pagination**: Use cursor-based pagination (Relay spec)6. **Deprecation**: Use `@deprecated` directive for gradual migration7. **Monitoring**: Track query complexity and execution time ## Common Pitfalls - **Over-fetching/Under-fetching (REST)**: Fixed in GraphQL but requires DataLoaders- **Breaking Changes**: Version APIs or use deprecation strategies- **Inconsistent Error Formats**: Standardize error responses- **Missing Rate Limits**: APIs without limits are vulnerable to abuse- **Poor Documentation**: Undocumented APIs frustrate developers- **Ignoring HTTP Semantics**: POST for idempotent operations breaks expectations- **Tight Coupling**: API structure shouldn't mirror database schema