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.
references/details.md
1# api-design-principles — detailed patterns and worked examples23## REST API Design Patterns45### Pattern 1: Resource Collection Design67```python8# Good: Resource-oriented endpoints9GET /api/users # List users (with pagination)10POST /api/users # Create user11GET /api/users/{id} # Get specific user12PUT /api/users/{id} # Replace user13PATCH /api/users/{id} # Update user fields14DELETE /api/users/{id} # Delete user1516# Nested resources17GET /api/users/{id}/orders # Get user's orders18POST /api/users/{id}/orders # Create order for user1920# Bad: Action-oriented endpoints (avoid)21POST /api/createUser22POST /api/getUserById23POST /api/deleteUser24```2526### Pattern 2: Pagination and Filtering2728```python29from typing import List, Optional30from pydantic import BaseModel, Field3132class PaginationParams(BaseModel):33page: int = Field(1, ge=1, description="Page number")34page_size: int = Field(20, ge=1, le=100, description="Items per page")3536class FilterParams(BaseModel):37status: Optional[str] = None38created_after: Optional[str] = None39search: Optional[str] = None4041class PaginatedResponse(BaseModel):42items: List[dict]43total: int44page: int45page_size: int46pages: int4748@property49def has_next(self) -> bool:50return self.page < self.pages5152@property53def has_prev(self) -> bool:54return self.page > 15556# FastAPI endpoint example57from fastapi import FastAPI, Query, Depends5859app = FastAPI()6061@app.get("/api/users", response_model=PaginatedResponse)62async def list_users(63page: int = Query(1, ge=1),64page_size: int = Query(20, ge=1, le=100),65status: Optional[str] = Query(None),66search: Optional[str] = Query(None)67):68# Apply filters69query = build_query(status=status, search=search)7071# Count total72total = await count_users(query)7374# Fetch page75offset = (page - 1) * page_size76users = await fetch_users(query, limit=page_size, offset=offset)7778return PaginatedResponse(79items=users,80total=total,81page=page,82page_size=page_size,83pages=(total + page_size - 1) // page_size84)85```8687### Pattern 3: Error Handling and Status Codes8889```python90from fastapi import HTTPException, status91from pydantic import BaseModel9293class ErrorResponse(BaseModel):94error: str95message: str96details: Optional[dict] = None97timestamp: str98path: str99100class ValidationErrorDetail(BaseModel):101field: str102message: str103value: Any104105# Consistent error responses106STATUS_CODES = {107"success": 200,108"created": 201,109"no_content": 204,110"bad_request": 400,111"unauthorized": 401,112"forbidden": 403,113"not_found": 404,114"conflict": 409,115"unprocessable": 422,116"internal_error": 500117}118119def raise_not_found(resource: str, id: str):120raise HTTPException(121status_code=status.HTTP_404_NOT_FOUND,122detail={123"error": "NotFound",124"message": f"{resource} not found",125"details": {"id": id}126}127)128129def raise_validation_error(errors: List[ValidationErrorDetail]):130raise HTTPException(131status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,132detail={133"error": "ValidationError",134"message": "Request validation failed",135"details": {"errors": [e.dict() for e in errors]}136}137)138139# Example usage140@app.get("/api/users/{user_id}")141async def get_user(user_id: str):142user = await fetch_user(user_id)143if not user:144raise_not_found("User", user_id)145return user146```147148### Pattern 4: HATEOAS (Hypermedia as the Engine of Application State)149150```python151class UserResponse(BaseModel):152id: str153name: str154email: str155_links: dict156157@classmethod158def from_user(cls, user: User, base_url: str):159return cls(160id=user.id,161name=user.name,162email=user.email,163_links={164"self": {"href": f"{base_url}/api/users/{user.id}"},165"orders": {"href": f"{base_url}/api/users/{user.id}/orders"},166"update": {167"href": f"{base_url}/api/users/{user.id}",168"method": "PATCH"169},170"delete": {171"href": f"{base_url}/api/users/{user.id}",172"method": "DELETE"173}174}175)176```177178## GraphQL Design Patterns179180### Pattern 1: Schema Design181182```graphql183# schema.graphql184185# Clear type definitions186type User {187id: ID!188email: String!189name: String!190createdAt: DateTime!191192# Relationships193orders(first: Int = 20, after: String, status: OrderStatus): OrderConnection!194195profile: UserProfile196}197198type Order {199id: ID!200status: OrderStatus!201total: Money!202items: [OrderItem!]!203createdAt: DateTime!204205# Back-reference206user: User!207}208209# Pagination pattern (Relay-style)210type OrderConnection {211edges: [OrderEdge!]!212pageInfo: PageInfo!213totalCount: Int!214}215216type OrderEdge {217node: Order!218cursor: String!219}220221type PageInfo {222hasNextPage: Boolean!223hasPreviousPage: Boolean!224startCursor: String225endCursor: String226}227228# Enums for type safety229enum OrderStatus {230PENDING231CONFIRMED232SHIPPED233DELIVERED234CANCELLED235}236237# Custom scalars238scalar DateTime239scalar Money240241# Query root242type Query {243user(id: ID!): User244users(first: Int = 20, after: String, search: String): UserConnection!245246order(id: ID!): Order247}248249# Mutation root250type Mutation {251createUser(input: CreateUserInput!): CreateUserPayload!252updateUser(input: UpdateUserInput!): UpdateUserPayload!253deleteUser(id: ID!): DeleteUserPayload!254255createOrder(input: CreateOrderInput!): CreateOrderPayload!256}257258# Input types for mutations259input CreateUserInput {260email: String!261name: String!262password: String!263}264265# Payload types for mutations266type CreateUserPayload {267user: User268errors: [Error!]269}270271type Error {272field: String273message: String!274}275```276277### Pattern 2: Resolver Design278279```python280from typing import Optional, List281from ariadne import QueryType, MutationType, ObjectType282from dataclasses import dataclass283284query = QueryType()285mutation = MutationType()286user_type = ObjectType("User")287288@query.field("user")289async def resolve_user(obj, info, id: str) -> Optional[dict]:290"""Resolve single user by ID."""291return await fetch_user_by_id(id)292293@query.field("users")294async def resolve_users(295obj,296info,297first: int = 20,298after: Optional[str] = None,299search: Optional[str] = None300) -> dict:301"""Resolve paginated user list."""302# Decode cursor303offset = decode_cursor(after) if after else 0304305# Fetch users306users = await fetch_users(307limit=first + 1, # Fetch one extra to check hasNextPage308offset=offset,309search=search310)311312# Pagination313has_next = len(users) > first314if has_next:315users = users[:first]316317edges = [318{319"node": user,320"cursor": encode_cursor(offset + i)321}322for i, user in enumerate(users)323]324325return {326"edges": edges,327"pageInfo": {328"hasNextPage": has_next,329"hasPreviousPage": offset > 0,330"startCursor": edges[0]["cursor"] if edges else None,331"endCursor": edges[-1]["cursor"] if edges else None332},333"totalCount": await count_users(search=search)334}335336@user_type.field("orders")337async def resolve_user_orders(user: dict, info, first: int = 20) -> dict:338"""Resolve user's orders (N+1 prevention with DataLoader)."""339# Use DataLoader to batch requests340loader = info.context["loaders"]["orders_by_user"]341orders = await loader.load(user["id"])342343return paginate_orders(orders, first)344345@mutation.field("createUser")346async def resolve_create_user(obj, info, input: dict) -> dict:347"""Create new user."""348try:349# Validate input350validate_user_input(input)351352# Create user353user = await create_user(354email=input["email"],355name=input["name"],356password=hash_password(input["password"])357)358359return {360"user": user,361"errors": []362}363except ValidationError as e:364return {365"user": None,366"errors": [{"field": e.field, "message": e.message}]367}368```369370### Pattern 3: DataLoader (N+1 Problem Prevention)371372```python373from aiodataloader import DataLoader374from typing import List, Optional375376class UserLoader(DataLoader):377"""Batch load users by ID."""378379async def batch_load_fn(self, user_ids: List[str]) -> List[Optional[dict]]:380"""Load multiple users in single query."""381users = await fetch_users_by_ids(user_ids)382383# Map results back to input order384user_map = {user["id"]: user for user in users}385return [user_map.get(user_id) for user_id in user_ids]386387class OrdersByUserLoader(DataLoader):388"""Batch load orders by user ID."""389390async def batch_load_fn(self, user_ids: List[str]) -> List[List[dict]]:391"""Load orders for multiple users in single query."""392orders = await fetch_orders_by_user_ids(user_ids)393394# Group orders by user_id395orders_by_user = {}396for order in orders:397user_id = order["user_id"]398if user_id not in orders_by_user:399orders_by_user[user_id] = []400orders_by_user[user_id].append(order)401402# Return in input order403return [orders_by_user.get(user_id, []) for user_id in user_ids]404405# Context setup406def create_context():407return {408"loaders": {409"user": UserLoader(),410"orders_by_user": OrdersByUserLoader()411}412}413```414