Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Design RESTful and GraphQL APIs following industry best practices for consistency, versioning, and developer experience.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
SKILL.md
1---2name: api-design-principles3description: 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.4---56# API Design Principles78Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers and stand the test of time.910## When to Use This Skill1112- Designing new REST or GraphQL APIs13- Refactoring existing APIs for better usability14- Establishing API design standards for your team15- Reviewing API specifications before implementation16- Migrating between API paradigms (REST to GraphQL, etc.)17- Creating developer-friendly API documentation18- Optimizing APIs for specific use cases (mobile, third-party integrations)1920## Core Concepts2122### 1. RESTful Design Principles2324**Resource-Oriented Architecture**2526- Resources are nouns (users, orders, products), not verbs27- Use HTTP methods for actions (GET, POST, PUT, PATCH, DELETE)28- URLs represent resource hierarchies29- Consistent naming conventions3031**HTTP Methods Semantics:**3233- `GET`: Retrieve resources (idempotent, safe)34- `POST`: Create new resources35- `PUT`: Replace entire resource (idempotent)36- `PATCH`: Partial resource updates37- `DELETE`: Remove resources (idempotent)3839### 2. GraphQL Design Principles4041**Schema-First Development**4243- Types define your domain model44- Queries for reading data45- Mutations for modifying data46- Subscriptions for real-time updates4748**Query Structure:**4950- Clients request exactly what they need51- Single endpoint, multiple operations52- Strongly typed schema53- Introspection built-in5455### 3. API Versioning Strategies5657**URL Versioning:**5859```60/api/v1/users61/api/v2/users62```6364**Header Versioning:**6566```67Accept: application/vnd.api+json; version=168```6970**Query Parameter Versioning:**7172```73/api/users?version=174```7576## REST API Design Patterns7778### Pattern 1: Resource Collection Design7980```python81# Good: Resource-oriented endpoints82GET /api/users # List users (with pagination)83POST /api/users # Create user84GET /api/users/{id} # Get specific user85PUT /api/users/{id} # Replace user86PATCH /api/users/{id} # Update user fields87DELETE /api/users/{id} # Delete user8889# Nested resources90GET /api/users/{id}/orders # Get user's orders91POST /api/users/{id}/orders # Create order for user9293# Bad: Action-oriented endpoints (avoid)94POST /api/createUser95POST /api/getUserById96POST /api/deleteUser97```9899### Pattern 2: Pagination and Filtering100101```python102from typing import List, Optional103from pydantic import BaseModel, Field104105class PaginationParams(BaseModel):106page: int = Field(1, ge=1, description="Page number")107page_size: int = Field(20, ge=1, le=100, description="Items per page")108109class FilterParams(BaseModel):110status: Optional[str] = None111created_after: Optional[str] = None112search: Optional[str] = None113114class PaginatedResponse(BaseModel):115items: List[dict]116total: int117page: int118page_size: int119pages: int120121@property122def has_next(self) -> bool:123return self.page < self.pages124125@property126def has_prev(self) -> bool:127return self.page > 1128129# FastAPI endpoint example130from fastapi import FastAPI, Query, Depends131132app = FastAPI()133134@app.get("/api/users", response_model=PaginatedResponse)135async def list_users(136page: int = Query(1, ge=1),137page_size: int = Query(20, ge=1, le=100),138status: Optional[str] = Query(None),139search: Optional[str] = Query(None)140):141# Apply filters142query = build_query(status=status, search=search)143144# Count total145total = await count_users(query)146147# Fetch page148offset = (page - 1) * page_size149users = await fetch_users(query, limit=page_size, offset=offset)150151return PaginatedResponse(152items=users,153total=total,154page=page,155page_size=page_size,156pages=(total + page_size - 1) // page_size157)158```159160### Pattern 3: Error Handling and Status Codes161162```python163from fastapi import HTTPException, status164from pydantic import BaseModel165166class ErrorResponse(BaseModel):167error: str168message: str169details: Optional[dict] = None170timestamp: str171path: str172173class ValidationErrorDetail(BaseModel):174field: str175message: str176value: Any177178# Consistent error responses179STATUS_CODES = {180"success": 200,181"created": 201,182"no_content": 204,183"bad_request": 400,184"unauthorized": 401,185"forbidden": 403,186"not_found": 404,187"conflict": 409,188"unprocessable": 422,189"internal_error": 500190}191192def raise_not_found(resource: str, id: str):193raise HTTPException(194status_code=status.HTTP_404_NOT_FOUND,195detail={196"error": "NotFound",197"message": f"{resource} not found",198"details": {"id": id}199}200)201202def raise_validation_error(errors: List[ValidationErrorDetail]):203raise HTTPException(204status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,205detail={206"error": "ValidationError",207"message": "Request validation failed",208"details": {"errors": [e.dict() for e in errors]}209}210)211212# Example usage213@app.get("/api/users/{user_id}")214async def get_user(user_id: str):215user = await fetch_user(user_id)216if not user:217raise_not_found("User", user_id)218return user219```220221### Pattern 4: HATEOAS (Hypermedia as the Engine of Application State)222223```python224class UserResponse(BaseModel):225id: str226name: str227email: str228_links: dict229230@classmethod231def from_user(cls, user: User, base_url: str):232return cls(233id=user.id,234name=user.name,235email=user.email,236_links={237"self": {"href": f"{base_url}/api/users/{user.id}"},238"orders": {"href": f"{base_url}/api/users/{user.id}/orders"},239"update": {240"href": f"{base_url}/api/users/{user.id}",241"method": "PATCH"242},243"delete": {244"href": f"{base_url}/api/users/{user.id}",245"method": "DELETE"246}247}248)249```250251## GraphQL Design Patterns252253### Pattern 1: Schema Design254255```graphql256# schema.graphql257258# Clear type definitions259type User {260id: ID!261email: String!262name: String!263createdAt: DateTime!264265# Relationships266orders(first: Int = 20, after: String, status: OrderStatus): OrderConnection!267268profile: UserProfile269}270271type Order {272id: ID!273status: OrderStatus!274total: Money!275items: [OrderItem!]!276createdAt: DateTime!277278# Back-reference279user: User!280}281282# Pagination pattern (Relay-style)283type OrderConnection {284edges: [OrderEdge!]!285pageInfo: PageInfo!286totalCount: Int!287}288289type OrderEdge {290node: Order!291cursor: String!292}293294type PageInfo {295hasNextPage: Boolean!296hasPreviousPage: Boolean!297startCursor: String298endCursor: String299}300301# Enums for type safety302enum OrderStatus {303PENDING304CONFIRMED305SHIPPED306DELIVERED307CANCELLED308}309310# Custom scalars311scalar DateTime312scalar Money313314# Query root315type Query {316user(id: ID!): User317users(first: Int = 20, after: String, search: String): UserConnection!318319order(id: ID!): Order320}321322# Mutation root323type Mutation {324createUser(input: CreateUserInput!): CreateUserPayload!325updateUser(input: UpdateUserInput!): UpdateUserPayload!326deleteUser(id: ID!): DeleteUserPayload!327328createOrder(input: CreateOrderInput!): CreateOrderPayload!329}330331# Input types for mutations332input CreateUserInput {333email: String!334name: String!335password: String!336}337338# Payload types for mutations339type CreateUserPayload {340user: User341errors: [Error!]342}343344type Error {345field: String346message: String!347}348```349350### Pattern 2: Resolver Design351352```python353from typing import Optional, List354from ariadne import QueryType, MutationType, ObjectType355from dataclasses import dataclass356357query = QueryType()358mutation = MutationType()359user_type = ObjectType("User")360361@query.field("user")362async def resolve_user(obj, info, id: str) -> Optional[dict]:363"""Resolve single user by ID."""364return await fetch_user_by_id(id)365366@query.field("users")367async def resolve_users(368obj,369info,370first: int = 20,371after: Optional[str] = None,372search: Optional[str] = None373) -> dict:374"""Resolve paginated user list."""375# Decode cursor376offset = decode_cursor(after) if after else 0377378# Fetch users379users = await fetch_users(380limit=first + 1, # Fetch one extra to check hasNextPage381offset=offset,382search=search383)384385# Pagination386has_next = len(users) > first387if has_next:388users = users[:first]389390edges = [391{392"node": user,393"cursor": encode_cursor(offset + i)394}395for i, user in enumerate(users)396]397398return {399"edges": edges,400"pageInfo": {401"hasNextPage": has_next,402"hasPreviousPage": offset > 0,403"startCursor": edges[0]["cursor"] if edges else None,404"endCursor": edges[-1]["cursor"] if edges else None405},406"totalCount": await count_users(search=search)407}408409@user_type.field("orders")410async def resolve_user_orders(user: dict, info, first: int = 20) -> dict:411"""Resolve user's orders (N+1 prevention with DataLoader)."""412# Use DataLoader to batch requests413loader = info.context["loaders"]["orders_by_user"]414orders = await loader.load(user["id"])415416return paginate_orders(orders, first)417418@mutation.field("createUser")419async def resolve_create_user(obj, info, input: dict) -> dict:420"""Create new user."""421try:422# Validate input423validate_user_input(input)424425# Create user426user = await create_user(427email=input["email"],428name=input["name"],429password=hash_password(input["password"])430)431432return {433"user": user,434"errors": []435}436except ValidationError as e:437return {438"user": None,439"errors": [{"field": e.field, "message": e.message}]440}441```442443### Pattern 3: DataLoader (N+1 Problem Prevention)444445```python446from aiodataloader import DataLoader447from typing import List, Optional448449class UserLoader(DataLoader):450"""Batch load users by ID."""451452async def batch_load_fn(self, user_ids: List[str]) -> List[Optional[dict]]:453"""Load multiple users in single query."""454users = await fetch_users_by_ids(user_ids)455456# Map results back to input order457user_map = {user["id"]: user for user in users}458return [user_map.get(user_id) for user_id in user_ids]459460class OrdersByUserLoader(DataLoader):461"""Batch load orders by user ID."""462463async def batch_load_fn(self, user_ids: List[str]) -> List[List[dict]]:464"""Load orders for multiple users in single query."""465orders = await fetch_orders_by_user_ids(user_ids)466467# Group orders by user_id468orders_by_user = {}469for order in orders:470user_id = order["user_id"]471if user_id not in orders_by_user:472orders_by_user[user_id] = []473orders_by_user[user_id].append(order)474475# Return in input order476return [orders_by_user.get(user_id, []) for user_id in user_ids]477478# Context setup479def create_context():480return {481"loaders": {482"user": UserLoader(),483"orders_by_user": OrdersByUserLoader()484}485}486```487488## Best Practices489490### REST APIs4914921. **Consistent Naming**: Use plural nouns for collections (`/users`, not `/user`)4932. **Stateless**: Each request contains all necessary information4943. **Use HTTP Status Codes Correctly**: 2xx success, 4xx client errors, 5xx server errors4954. **Version Your API**: Plan for breaking changes from day one4965. **Pagination**: Always paginate large collections4976. **Rate Limiting**: Protect your API with rate limits4987. **Documentation**: Use OpenAPI/Swagger for interactive docs499500### GraphQL APIs5015021. **Schema First**: Design schema before writing resolvers5032. **Avoid N+1**: Use DataLoaders for efficient data fetching5043. **Input Validation**: Validate at schema and resolver levels5054. **Error Handling**: Return structured errors in mutation payloads5065. **Pagination**: Use cursor-based pagination (Relay spec)5076. **Deprecation**: Use `@deprecated` directive for gradual migration5087. **Monitoring**: Track query complexity and execution time509510## Common Pitfalls511512- **Over-fetching/Under-fetching (REST)**: Fixed in GraphQL but requires DataLoaders513- **Breaking Changes**: Version APIs or use deprecation strategies514- **Inconsistent Error Formats**: Standardize error responses515- **Missing Rate Limits**: APIs without limits are vulnerable to abuse516- **Poor Documentation**: Undocumented APIs frustrate developers517- **Ignoring HTTP Semantics**: POST for idempotent operations breaks expectations518- **Tight Coupling**: API structure shouldn't mirror database schema519