api-design-principles
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.
Install
npx skills add https://github.com/wshobson/agents/tree/main/plugins/backend-development/skills/api-design-principles
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wshobson-agents@llmmart
git clone https://github.com/wshobson/agents.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wshobson/agents collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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 resourcesPUT: Replace entire resource (idempotent)PATCH: Partial resource updatesDELETE: 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
Detailed patterns and worked examples
Detailed pattern documentation lives in references/details.md. Read that file when the navigation tier above is insufficient.
Best Practices
REST APIs
- Consistent Naming: Use plural nouns for collections (
/users, not/user) - Stateless: Each request contains all necessary information
- Use HTTP Status Codes Correctly: 2xx success, 4xx client errors, 5xx server errors
- Version Your API: Plan for breaking changes from day one
- Pagination: Always paginate large collections
- Rate Limiting: Protect your API with rate limits
- Documentation: Use OpenAPI/Swagger for interactive docs
GraphQL APIs
- Schema First: Design schema before writing resolvers
- Avoid N+1: Use DataLoaders for efficient data fetching
- Input Validation: Validate at schema and resolver levels
- Error Handling: Return structured errors in mutation payloads
- Pagination: Use cursor-based pagination (Relay spec)
- Deprecation: Use
@deprecateddirective for gradual migration - 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
Files (agents)
-
assets
-
api-design-checklist.md 3.8 KB
# API Design Checklist ## Pre-Implementation Review ### Resource Design - [ ] Resources are nouns, not verbs - [ ] Plural names for collections - [ ] Consistent naming across all endpoints - [ ] Clear resource hierarchy (avoid deep nesting >2 levels) - [ ] All CRUD operations properly mapped to HTTP methods ### HTTP Methods - [ ] GET for retrieval (safe, idempotent) - [ ] POST for creation - [ ] PUT for full replacement (idempotent) - [ ] PATCH for partial updates - [ ] DELETE for removal (idempotent) ### Status Codes - [ ] 200 OK for successful GET/PATCH/PUT - [ ] 201 Created for POST - [ ] 204 No Content for DELETE - [ ] 400 Bad Request for malformed requests - [ ] 401 Unauthorized for missing auth - [ ] 403 Forbidden for insufficient permissions - [ ] 404 Not Found for missing resources - [ ] 422 Unprocessable Entity for validation errors - [ ] 429 Too Many Requests for rate limiting - [ ] 500 Internal Server Error for server issues ### Pagination - [ ] All collection endpoints paginated - [ ] Default page size defined (e.g., 20) - [ ] Maximum page size enforced (e.g., 100) - [ ] Pagination metadata included (total, pages, etc.) - [ ] Cursor-based or offset-based pattern chosen ### Filtering & Sorting - [ ] Query parameters for filtering - [ ] Sort parameter supported - [ ] Search parameter for full-text search - [ ] Field selection supported (sparse fieldsets) ### Versioning - [ ] Versioning strategy defined (URL/header/query) - [ ] Version included in all endpoints - [ ] Deprecation policy documented ### Error Handling - [ ] Consistent error response format - [ ] Detailed error messages - [ ] Field-level validation errors - [ ] Error codes for client handling - [ ] Timestamps in error responses ### Authentication & Authorization - [ ] Authentication method defined (Bearer token, API key) - [ ] Authorization checks on all endpoints - [ ] 401 vs 403 used correctly - [ ] Token expiration handled ### Rate Limiting - [ ] Rate limits defined per endpoint/user - [ ] Rate limit headers included - [ ] 429 status code for exceeded limits - [ ] Retry-After header provided ### Documentation - [ ] OpenAPI/Swagger spec generated - [ ] All endpoints documented - [ ] Request/response examples provided - [ ] Error responses documented - [ ] Authentication flow documented ### Testing - [ ] Unit tests for business logic - [ ] Integration tests for endpoints - [ ] Error scenarios tested - [ ] Edge cases covered - [ ] Performance tests for heavy endpoints ### Security - [ ] Input validation on all fields - [ ] SQL injection prevention - [ ] XSS prevention - [ ] CORS configured correctly - [ ] HTTPS enforced - [ ] Sensitive data not in URLs - [ ] No secrets in responses ### Performance - [ ] Database queries optimized - [ ] N+1 queries prevented - [ ] Caching strategy defined - [ ] Cache headers set appropriately - [ ] Large responses paginated ### Monitoring - [ ] Logging implemented - [ ] Error tracking configured - [ ] Performance metrics collected - [ ] Health check endpoint available - [ ] Alerts configured for errors ## GraphQL-Specific Checks ### Schema Design - [ ] Schema-first approach used - [ ] Types properly defined - [ ] Non-null vs nullable decided - [ ] Interfaces/unions used appropriately - [ ] Custom scalars defined ### Queries - [ ] Query depth limiting - [ ] Query complexity analysis - [ ] DataLoaders prevent N+1 - [ ] Pagination pattern chosen (Relay/offset) ### Mutations - [ ] Input types defined - [ ] Payload types with errors - [ ] Optimistic response support - [ ] Idempotency considered ### Performance - [ ] DataLoader for all relationships - [ ] Query batching enabled - [ ] Persisted queries considered - [ ] Response caching implemented ### Documentation - [ ] All fields documented - [ ] Deprecations marked - [ ] Examples provided - [ ] Schema introspection enabled -
rest-api-template.py 5.2 KB
""" Production-ready REST API template using FastAPI. Includes pagination, filtering, error handling, and best practices. """ from fastapi import FastAPI, HTTPException, Query, Path, Depends, status from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.trustedhost import TrustedHostMiddleware from fastapi.responses import JSONResponse from pydantic import BaseModel, Field, EmailStr, ConfigDict from typing import Optional, List, Any from datetime import datetime from enum import Enum app = FastAPI( title="API Template", version="1.0.0", docs_url="/api/docs" ) # Security Middleware # Trusted Host: Prevents HTTP Host Header attacks app.add_middleware( TrustedHostMiddleware, allowed_hosts=["*"] # TODO: Configure this in production, e.g. ["api.example.com"] ) # CORS: Configures Cross-Origin Resource Sharing app.add_middleware( CORSMiddleware, allow_origins=["*"], # TODO: Update this with specific origins in production allow_credentials=False, # TODO: Set to True if you need cookies/auth headers, but restrict origins allow_methods=["*"], allow_headers=["*"], ) # Models class UserStatus(str, Enum): ACTIVE = "active" INACTIVE = "inactive" SUSPENDED = "suspended" class UserBase(BaseModel): email: EmailStr name: str = Field(..., min_length=1, max_length=100) status: UserStatus = UserStatus.ACTIVE class UserCreate(UserBase): password: str = Field(..., min_length=8) class UserUpdate(BaseModel): email: Optional[EmailStr] = None name: Optional[str] = Field(None, min_length=1, max_length=100) status: Optional[UserStatus] = None class User(UserBase): id: str created_at: datetime updated_at: datetime model_config = ConfigDict(from_attributes=True) # Pagination class PaginationParams(BaseModel): page: int = Field(1, ge=1) page_size: int = Field(20, ge=1, le=100) class PaginatedResponse(BaseModel): items: List[Any] total: int page: int page_size: int pages: int # Error handling class ErrorDetail(BaseModel): field: Optional[str] = None message: str code: str class ErrorResponse(BaseModel): error: str message: str details: Optional[List[ErrorDetail]] = None @app.exception_handler(HTTPException) async def http_exception_handler(request, exc): return JSONResponse( status_code=exc.status_code, content=ErrorResponse( error=exc.__class__.__name__, message=exc.detail if isinstance(exc.detail, str) else exc.detail.get("message", "Error"), details=exc.detail.get("details") if isinstance(exc.detail, dict) else None ).model_dump() ) # Endpoints @app.get("/api/users", response_model=PaginatedResponse, tags=["Users"]) async def list_users( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), status: Optional[UserStatus] = Query(None), search: Optional[str] = Query(None) ): """List users with pagination and filtering.""" # Mock implementation total = 100 items = [ User( id=str(i), email=f"user{i}@example.com", name=f"User {i}", status=UserStatus.ACTIVE, created_at=datetime.now(), updated_at=datetime.now() ).model_dump() for i in range((page-1)*page_size, min(page*page_size, total)) ] return PaginatedResponse( items=items, total=total, page=page, page_size=page_size, pages=(total + page_size - 1) // page_size ) @app.post("/api/users", response_model=User, status_code=status.HTTP_201_CREATED, tags=["Users"]) async def create_user(user: UserCreate): """Create a new user.""" # Mock implementation return User( id="123", email=user.email, name=user.name, status=user.status, created_at=datetime.now(), updated_at=datetime.now() ) @app.get("/api/users/{user_id}", response_model=User, tags=["Users"]) async def get_user(user_id: str = Path(..., description="User ID")): """Get user by ID.""" # Mock: Check if exists if user_id == "999": raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail={"message": "User not found", "details": {"id": user_id}} ) return User( id=user_id, email="user@example.com", name="User Name", status=UserStatus.ACTIVE, created_at=datetime.now(), updated_at=datetime.now() ) @app.patch("/api/users/{user_id}", response_model=User, tags=["Users"]) async def update_user(user_id: str, update: UserUpdate): """Partially update user.""" # Validate user exists existing = await get_user(user_id) # Apply updates update_data = update.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(existing, field, value) existing.updated_at = datetime.now() return existing @app.delete("/api/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Users"]) async def delete_user(user_id: str): """Delete user.""" await get_user(user_id) # Verify exists return None if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)
-
-
references
-
details.md 9.6 KB
# api-design-principles — detailed patterns and worked examples ## REST API Design Patterns ### Pattern 1: Resource Collection Design ```python # Good: Resource-oriented endpoints GET /api/users # List users (with pagination) POST /api/users # Create user GET /api/users/{id} # Get specific user PUT /api/users/{id} # Replace user PATCH /api/users/{id} # Update user fields DELETE /api/users/{id} # Delete user # Nested resources GET /api/users/{id}/orders # Get user's orders POST /api/users/{id}/orders # Create order for user # Bad: Action-oriented endpoints (avoid) POST /api/createUser POST /api/getUserById POST /api/deleteUser ``` ### Pattern 2: Pagination and Filtering ```python from typing import List, Optional from 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 example from 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 ```python from fastapi import HTTPException, status from 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 responses STATUS_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) ```python class 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 definitions type 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 safety enum OrderStatus { PENDING CONFIRMED SHIPPED DELIVERED CANCELLED } # Custom scalars scalar DateTime scalar Money # Query root type Query { user(id: ID!): User users(first: Int = 20, after: String, search: String): UserConnection! order(id: ID!): Order } # Mutation root type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! updateUser(input: UpdateUserInput!): UpdateUserPayload! deleteUser(id: ID!): DeleteUserPayload! createOrder(input: CreateOrderInput!): CreateOrderPayload! } # Input types for mutations input CreateUserInput { email: String! name: String! password: String! } # Payload types for mutations type CreateUserPayload { user: User errors: [Error!] } type Error { field: String message: String! } ``` ### Pattern 2: Resolver Design ```python from typing import Optional, List from ariadne import QueryType, MutationType, ObjectType from 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) ```python from aiodataloader import DataLoader from 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 setup def create_context(): return { "loaders": { "user": UserLoader(), "orders_by_user": OrdersByUserLoader() } } ``` -
graphql-schema-design.md 8.8 KB
# GraphQL Schema Design Patterns ## Schema Organization ### Modular Schema Structure ```graphql # user.graphql type User { id: ID! email: String! name: String! posts: [Post!]! } extend type Query { user(id: ID!): User users(first: Int, after: String): UserConnection! } extend type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! } # post.graphql type Post { id: ID! title: String! content: String! author: User! } extend type Query { post(id: ID!): Post } ``` ## Type Design Patterns ### 1. Non-Null Types ```graphql type User { id: ID! # Always required email: String! # Required phone: String # Optional (nullable) posts: [Post!]! # Non-null array of non-null posts tags: [String!] # Nullable array of non-null strings } ``` ### 2. Interfaces for Polymorphism ```graphql interface Node { id: ID! createdAt: DateTime! } type User implements Node { id: ID! createdAt: DateTime! email: String! } type Post implements Node { id: ID! createdAt: DateTime! title: String! } type Query { node(id: ID!): Node } ``` ### 3. Unions for Heterogeneous Results ```graphql union SearchResult = User | Post | Comment type Query { search(query: String!): [SearchResult!]! } # Query example { search(query: "graphql") { ... on User { name email } ... on Post { title content } ... on Comment { text author { name } } } } ``` ### 4. Input Types ```graphql input CreateUserInput { email: String! name: String! password: String! profileInput: ProfileInput } input ProfileInput { bio: String avatar: String website: String } input UpdateUserInput { id: ID! email: String name: String profileInput: ProfileInput } ``` ## Pagination Patterns ### Relay Cursor Pagination (Recommended) ```graphql type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! totalCount: Int! } type UserEdge { node: User! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } type Query { users(first: Int, after: String, last: Int, before: String): UserConnection! } # Usage { users(first: 10, after: "cursor123") { edges { cursor node { id name } } pageInfo { hasNextPage endCursor } } } ``` ### Offset Pagination (Simpler) ```graphql type UserList { items: [User!]! total: Int! page: Int! pageSize: Int! } type Query { users(page: Int = 1, pageSize: Int = 20): UserList! } ``` ## Mutation Design Patterns ### 1. Input/Payload Pattern ```graphql input CreatePostInput { title: String! content: String! tags: [String!] } type CreatePostPayload { post: Post errors: [Error!] success: Boolean! } type Error { field: String message: String! code: String! } type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! } ``` ### 2. Optimistic Response Support ```graphql type UpdateUserPayload { user: User clientMutationId: String errors: [Error!] } input UpdateUserInput { id: ID! name: String clientMutationId: String } type Mutation { updateUser(input: UpdateUserInput!): UpdateUserPayload! } ``` ### 3. Batch Mutations ```graphql input BatchCreateUserInput { users: [CreateUserInput!]! } type BatchCreateUserPayload { results: [CreateUserResult!]! successCount: Int! errorCount: Int! } type CreateUserResult { user: User errors: [Error!] index: Int! } type Mutation { batchCreateUsers(input: BatchCreateUserInput!): BatchCreateUserPayload! } ``` ## Field Design ### Arguments and Filtering ```graphql type Query { posts( # Pagination first: Int = 20 after: String # Filtering status: PostStatus authorId: ID tag: String # Sorting orderBy: PostOrderBy = CREATED_AT orderDirection: OrderDirection = DESC # Searching search: String ): PostConnection! } enum PostStatus { DRAFT PUBLISHED ARCHIVED } enum PostOrderBy { CREATED_AT UPDATED_AT TITLE } enum OrderDirection { ASC DESC } ``` ### Computed Fields ```graphql type User { firstName: String! lastName: String! fullName: String! # Computed in resolver posts: [Post!]! postCount: Int! # Computed, doesn't load all posts } type Post { likeCount: Int! commentCount: Int! isLikedByViewer: Boolean! # Context-dependent } ``` ## Subscriptions ```graphql type Subscription { postAdded: Post! postUpdated(postId: ID!): Post! userStatusChanged(userId: ID!): UserStatus! } type UserStatus { userId: ID! online: Boolean! lastSeen: DateTime! } # Client usage subscription { postAdded { id title author { name } } } ``` ## Custom Scalars ```graphql scalar DateTime scalar Email scalar URL scalar JSON scalar Money type User { email: Email! website: URL createdAt: DateTime! metadata: JSON } type Product { price: Money! } ``` ## Directives ### Built-in Directives ```graphql type User { name: String! email: String! @deprecated(reason: "Use emails field instead") emails: [String!]! # Conditional inclusion privateData: PrivateData @include(if: $isOwner) } # Query query GetUser($isOwner: Boolean!) { user(id: "123") { name privateData @include(if: $isOwner) { ssn } } } ``` ### Custom Directives ```graphql directive @auth(requires: Role = USER) on FIELD_DEFINITION enum Role { USER ADMIN MODERATOR } type Mutation { deleteUser(id: ID!): Boolean! @auth(requires: ADMIN) updateProfile(input: ProfileInput!): User! @auth } ``` ## Error Handling ### Union Error Pattern ```graphql type User { id: ID! email: String! } type ValidationError { field: String! message: String! } type NotFoundError { message: String! resourceType: String! resourceId: ID! } type AuthorizationError { message: String! } union UserResult = User | ValidationError | NotFoundError | AuthorizationError type Query { user(id: ID!): UserResult! } # Usage { user(id: "123") { ... on User { id email } ... on NotFoundError { message resourceType } ... on AuthorizationError { message } } } ``` ### Errors in Payload ```graphql type CreateUserPayload { user: User errors: [Error!] success: Boolean! } type Error { field: String message: String! code: ErrorCode! } enum ErrorCode { VALIDATION_ERROR UNAUTHORIZED NOT_FOUND INTERNAL_ERROR } ``` ## N+1 Query Problem Solutions ### DataLoader Pattern ```python from aiodataloader import DataLoader class PostLoader(DataLoader): async def batch_load_fn(self, post_ids): posts = await db.posts.find({"id": {"$in": post_ids}}) post_map = {post["id"]: post for post in posts} return [post_map.get(pid) for pid in post_ids] # Resolver @user_type.field("posts") async def resolve_posts(user, info): loader = info.context["loaders"]["post"] return await loader.load_many(user["post_ids"]) ``` ### Query Depth Limiting ```python from graphql import GraphQLError def depth_limit_validator(max_depth: int): def validate(context, node, ancestors): depth = len(ancestors) if depth > max_depth: raise GraphQLError( f"Query depth {depth} exceeds maximum {max_depth}" ) return validate ``` ### Query Complexity Analysis ```python def complexity_limit_validator(max_complexity: int): def calculate_complexity(node): # Each field = 1, lists multiply complexity = 1 if is_list_field(node): complexity *= get_list_size_arg(node) return complexity return validate_complexity ``` ## Schema Versioning ### Field Deprecation ```graphql type User { name: String! @deprecated(reason: "Use firstName and lastName") firstName: String! lastName: String! } ``` ### Schema Evolution ```graphql # v1 - Initial type User { name: String! } # v2 - Add optional field (backward compatible) type User { name: String! email: String } # v3 - Deprecate and add new field type User { name: String! @deprecated(reason: "Use firstName/lastName") firstName: String! lastName: String! email: String } ``` ## Best Practices Summary 1. **Nullable vs Non-Null**: Start nullable, make non-null when guaranteed 2. **Input Types**: Always use input types for mutations 3. **Payload Pattern**: Return errors in mutation payloads 4. **Pagination**: Use cursor-based for infinite scroll, offset for simple cases 5. **Naming**: Use camelCase for fields, PascalCase for types 6. **Deprecation**: Use `@deprecated` instead of removing fields 7. **DataLoaders**: Always use for relationships to prevent N+1 8. **Complexity Limits**: Protect against expensive queries 9. **Custom Scalars**: Use for domain-specific types (Email, DateTime) 10. **Documentation**: Document all fields with descriptions -
rest-best-practices.md 7.4 KB
# REST API Best Practices ## URL Structure ### Resource Naming ``` # Good - Plural nouns GET /api/users GET /api/orders GET /api/products # Bad - Verbs or mixed conventions GET /api/getUser GET /api/user (inconsistent singular) POST /api/createOrder ``` ### Nested Resources ``` # Shallow nesting (preferred) GET /api/users/{id}/orders GET /api/orders/{id} # Deep nesting (avoid) GET /api/users/{id}/orders/{orderId}/items/{itemId}/reviews # Better: GET /api/order-items/{id}/reviews ``` ## HTTP Methods and Status Codes ### GET - Retrieve Resources ``` GET /api/users → 200 OK (with list) GET /api/users/{id} → 200 OK or 404 Not Found GET /api/users?page=2 → 200 OK (paginated) ``` ### POST - Create Resources ``` POST /api/users Body: {"name": "John", "email": "john@example.com"} → 201 Created Location: /api/users/123 Body: {"id": "123", "name": "John", ...} POST /api/users (validation error) → 422 Unprocessable Entity Body: {"errors": [...]} ``` ### PUT - Replace Resources ``` PUT /api/users/{id} Body: {complete user object} → 200 OK (updated) → 404 Not Found (doesn't exist) # Must include ALL fields ``` ### PATCH - Partial Update ``` PATCH /api/users/{id} Body: {"name": "Jane"} (only changed fields) → 200 OK → 404 Not Found ``` ### DELETE - Remove Resources ``` DELETE /api/users/{id} → 204 No Content (deleted) → 404 Not Found → 409 Conflict (can't delete due to references) ``` ## Filtering, Sorting, and Searching ### Query Parameters ``` # Filtering GET /api/users?status=active GET /api/users?role=admin&status=active # Sorting GET /api/users?sort=created_at GET /api/users?sort=-created_at (descending) GET /api/users?sort=name,created_at # Searching GET /api/users?search=john GET /api/users?q=john # Field selection (sparse fieldsets) GET /api/users?fields=id,name,email ``` ## Pagination Patterns ### Offset-Based Pagination ```python GET /api/users?page=2&page_size=20 Response: { "items": [...], "page": 2, "page_size": 20, "total": 150, "pages": 8 } ``` ### Cursor-Based Pagination (for large datasets) ```python GET /api/users?limit=20&cursor=eyJpZCI6MTIzfQ Response: { "items": [...], "next_cursor": "eyJpZCI6MTQzfQ", "has_more": true } ``` ### Link Header Pagination (RESTful) ``` GET /api/users?page=2 Response Headers: Link: <https://api.example.com/users?page=3>; rel="next", <https://api.example.com/users?page=1>; rel="prev", <https://api.example.com/users?page=1>; rel="first", <https://api.example.com/users?page=8>; rel="last" ``` ## Versioning Strategies ### URL Versioning (Recommended) ``` /api/v1/users /api/v2/users Pros: Clear, easy to route Cons: Multiple URLs for same resource ``` ### Header Versioning ``` GET /api/users Accept: application/vnd.api+json; version=2 Pros: Clean URLs Cons: Less visible, harder to test ``` ### Query Parameter ``` GET /api/users?version=2 Pros: Easy to test Cons: Optional parameter can be forgotten ``` ## Rate Limiting ### Headers ``` X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 742 X-RateLimit-Reset: 1640000000 Response when limited: 429 Too Many Requests Retry-After: 3600 ``` ### Implementation Pattern ```python from fastapi import HTTPException, Request from datetime import datetime, timedelta class RateLimiter: def __init__(self, calls: int, period: int): self.calls = calls self.period = period self.cache = {} def check(self, key: str) -> bool: now = datetime.now() if key not in self.cache: self.cache[key] = [] # Remove old requests self.cache[key] = [ ts for ts in self.cache[key] if now - ts < timedelta(seconds=self.period) ] if len(self.cache[key]) >= self.calls: return False self.cache[key].append(now) return True limiter = RateLimiter(calls=100, period=60) @app.get("/api/users") async def get_users(request: Request): if not limiter.check(request.client.host): raise HTTPException( status_code=429, headers={"Retry-After": "60"} ) return {"users": [...]} ``` ## Authentication and Authorization ### Bearer Token ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIs... 401 Unauthorized - Missing/invalid token 403 Forbidden - Valid token, insufficient permissions ``` ### API Keys ``` X-API-Key: your-api-key-here ``` ## Error Response Format ### Consistent Structure ```json { "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "details": [ { "field": "email", "message": "Invalid email format", "value": "not-an-email" } ], "timestamp": "2025-10-16T12:00:00Z", "path": "/api/users" } } ``` ### Status Code Guidelines - `200 OK`: Successful GET, PATCH, PUT - `201 Created`: Successful POST - `204 No Content`: Successful DELETE - `400 Bad Request`: Malformed request - `401 Unauthorized`: Authentication required - `403 Forbidden`: Authenticated but not authorized - `404 Not Found`: Resource doesn't exist - `409 Conflict`: State conflict (duplicate email, etc.) - `422 Unprocessable Entity`: Validation errors - `429 Too Many Requests`: Rate limited - `500 Internal Server Error`: Server error - `503 Service Unavailable`: Temporary downtime ## Caching ### Cache Headers ``` # Client caching Cache-Control: public, max-age=3600 # No caching Cache-Control: no-cache, no-store, must-revalidate # Conditional requests ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4" → 304 Not Modified ``` ## Bulk Operations ### Batch Endpoints ```python POST /api/users/batch { "items": [ {"name": "User1", "email": "user1@example.com"}, {"name": "User2", "email": "user2@example.com"} ] } Response: { "results": [ {"id": "1", "status": "created"}, {"id": null, "status": "failed", "error": "Email already exists"} ] } ``` ## Idempotency ### Idempotency Keys ``` POST /api/orders Idempotency-Key: unique-key-123 If duplicate request: → 200 OK (return cached response) ``` ## CORS Configuration ```python from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["https://example.com"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) ``` ## Documentation with OpenAPI ```python from fastapi import FastAPI app = FastAPI( title="My API", description="API for managing users", version="1.0.0", docs_url="/docs", redoc_url="/redoc" ) @app.get( "/api/users/{user_id}", summary="Get user by ID", response_description="User details", tags=["Users"] ) async def get_user( user_id: str = Path(..., description="The user ID") ): """ Retrieve user by ID. Returns full user profile including: - Basic information - Contact details - Account status """ pass ``` ## Health and Monitoring Endpoints ```python @app.get("/health") async def health_check(): return { "status": "healthy", "version": "1.0.0", "timestamp": datetime.now().isoformat() } @app.get("/health/detailed") async def detailed_health(): return { "status": "healthy", "checks": { "database": await check_database(), "redis": await check_redis(), "external_api": await check_external_api() } } ```
-
-
SKILL.md 3.5 KB
--- name: api-design-principles description: 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 ``` ## Detailed patterns and worked examples Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient. ## Best Practices ### REST APIs 1. **Consistent Naming**: Use plural nouns for collections (`/users`, not `/user`) 2. **Stateless**: Each request contains all necessary information 3. **Use HTTP Status Codes Correctly**: 2xx success, 4xx client errors, 5xx server errors 4. **Version Your API**: Plan for breaking changes from day one 5. **Pagination**: Always paginate large collections 6. **Rate Limiting**: Protect your API with rate limits 7. **Documentation**: Use OpenAPI/Swagger for interactive docs ### GraphQL APIs 1. **Schema First**: Design schema before writing resolvers 2. **Avoid N+1**: Use DataLoaders for efficient data fetching 3. **Input Validation**: Validate at schema and resolver levels 4. **Error Handling**: Return structured errors in mutation payloads 5. **Pagination**: Use cursor-based pagination (Relay spec) 6. **Deprecation**: Use `@deprecated` directive for gradual migration 7. **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
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.