<< All versions
Skill v1.0.0
currentLLM-judged scan90/100the-aios/aios/api-design-principles
──Details
PublishedOctober 24, 2026 at 12:48 PM
Content Hashsha256:fa44aba6c2687e28...
Git SHAc0c91f830dbf
──Files
Files (1 file, 13.4 KB)
SKILL.md13.4 KBactive
SKILL.md · 530 lines · 13.4 KB
version: "1.0.0" 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 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
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
python
from typing import List, Optionalfrom pydantic import BaseModel, Fieldclass 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] = Nonecreated_after: Optional[str] = Nonesearch: Optional[str] = Noneclass PaginatedResponse(BaseModel):items: List[dict]total: intpage: intpage_size: intpages: int@propertydef has_next(self) -> bool:return self.page < self.pages@propertydef has_prev(self) -> bool:return self.page > 1# FastAPI endpoint examplefrom fastapi import FastAPI, Query, Dependsapp = 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 filtersquery = build_query(status=status, search=search)# Count totaltotal = await count_users(query)# Fetch pageoffset = (page - 1) * page_sizeusers = 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, statusfrom pydantic import BaseModelclass ErrorResponse(BaseModel):error: strmessage: strdetails: Optional[dict] = Nonetimestamp: strpath: strclass ValidationErrorDetail(BaseModel):field: strmessage: strvalue: 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)
python
class UserResponse(BaseModel):id: strname: stremail: str_links: dict@classmethoddef 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!# Relationshipsorders(first: Int = 20, after: String, status: OrderStatus): OrderConnection!profile: UserProfile}type Order {id: ID!status: OrderStatus!total: Money!items: [OrderItem!]!createdAt: DateTime!# Back-referenceuser: 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: StringendCursor: String}# Enums for type safetyenum OrderStatus {PENDINGCONFIRMEDSHIPPEDDELIVEREDCANCELLED}# Custom scalarsscalar DateTimescalar Money# Query roottype Query {user(id: ID!): Userusers(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: Usererrors: [Error!]}type Error {field: Stringmessage: String!}
Pattern 2: Resolver Design
python
from typing import Optional, Listfrom ariadne import QueryType, MutationType, ObjectTypefrom dataclasses import dataclassquery = 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 cursoroffset = decode_cursor(after) if after else 0# Fetch usersusers = await fetch_users(limit=first + 1, # Fetch one extra to check hasNextPageoffset=offset,search=search)# Paginationhas_next = len(users) > firstif 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 requestsloader = 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 inputvalidate_user_input(input)# Create useruser = 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 DataLoaderfrom typing import List, Optionalclass 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 orderuser_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_idorders_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 orderreturn [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
- 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
Resources
- references/rest-best-practices.md: Comprehensive REST API design guide
- references/graphql-schema-design.md: GraphQL schema patterns and anti-patterns
- references/api-versioning-strategies.md: Versioning approaches and migration paths
- assets/rest-api-template.py: FastAPI REST API template
- assets/graphql-schema-template.graphql: Complete GraphQL schema example
- assets/api-design-checklist.md: Pre-implementation review checklist
- scripts/openapi-generator.py: Generate OpenAPI specs from code