npx skills add https://github.com/wshobson/agents --skill api-design-principlesHow 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.
Pre-configured AI company — 18 agents, 18 skills, one-time purchase.
SKILL.md518 linesExpandCollapse
---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 schemaAccessibility Compliance
This walks you through implementing proper WCAG 2.2 compliance with real code patterns for screen readers, keyboard navigation, and mobile accessibility. It cov
Airflow Dag Patterns
If you're building data pipelines with Airflow, this skill gives you production-ready DAG patterns that actually work in the real world. It covers TaskFlow API
Angular Migration
Migrating from AngularJS to Angular is notoriously painful, and this skill tackles the practical stuff that makes or breaks these projects. It covers hybrid app