Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/asymmetric-al-api-design-principles/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 136% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 136% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 134% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 160% | 0% |
Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers and stand the test of time.
Resource-Oriented Architecture
HTTP Methods Semantics:
GET: Retrieve resources (idempotent, safe)POST: Create new resourcesPUT: Replace entire resource (idempotent)PATCH: Partial resource updatesDELETE: Remove resources (idempotent)Schema-First Development
Query Structure:
URL Versioning:
/api/v1/users
/api/v2/usersHeader Versioning:
Accept: application/vnd.api+json; version=1Query Parameter Versioning:
/api/users?version=1python# 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
pythonfrom 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 )
pythonfrom 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
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# 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! } type UserProfile { avatarUrl: String bio: String timezone: String } type OrderItem { id: ID! sku: String! name: String! quantity: Int! unitPrice: Money! } # 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! }
pythonfrom 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}] }
pythonfrom 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() } }
/users, not /user)@deprecated directive for gradual migration| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 18,360 | 21,083 | +15% | 1 | 1 | 0% | 3,267 | 7,711 | +136% | 0 | 0 | — |
case-02 | pass→pass | 16,546 | 15,753 | -5% | 1 | 1 | 0% | 2,942 | 6,890 | +134% | 0 | 0 | — |
case-03 | fail→fail | 17,290 | 13,456 | -22% | 1 | 1 | 0% | 2,944 | 6,133 | +108% | 0 | 0 | — |
case-04 | fail→fail | 14,086 | 10,786 | -23% | 1 | 1 | 0% | 2,460 | 5,612 | +128% | 0 | 0 | — |
case-05 | pass→pass | 16,159 | 18,803 | +16% | 1 | 1 | 0% | 2,708 | 7,054 | +160% | 0 | 0 | — |
case-06 | fail→pass | 15,763 | 13,695 | -13% | 1 | 1 | 0% | 2,702 | 6,378 | +136% | 0 | 0 | — |
case-07 | pass→pass | 13,569 | 9,231 | -32% | 1 | 1 | 0% | 2,438 | 5,438 | +123% | 0 | 0 | — |
case-08 | pass→pass | 10,872 | 10,890 | +0% | 1 | 1 | 0% | 1,858 | 5,737 | +209% | 0 | 0 | — |
case-09 | fail→fail | 12,103 | 10,527 | -13% | 1 | 1 | 0% | 2,135 | 5,525 | +159% | 0 | 0 | — |
case-10 | pass→pass | 14,082 | 11,973 | -15% | 1 | 1 | 0% | 2,370 | 5,873 | +148% | 0 | 0 | — |
case-21 | pass→pass | 7,580 | 7,691 | +1% | 1 | 1 | 0% | 1,786 | 5,576 | +212% | 0 | 0 | — |
case-11 | pass→pass | 13,913 | 11,884 | -15% | 1 | 1 | 0% | 2,582 | 6,000 | +132% | 0 | 0 | — |
case-12 | fail→fail | 11,706 | 10,423 | -11% | 1 | 1 | 0% | 2,002 | 5,419 | +171% | 0 | 0 | — |
case-13 | pass→pass | 15,907 | 17,133 | +8% | 1 | 1 | 0% | 2,966 | 6,988 | +136% | 0 | 0 | — |
case-14 | pass→pass | 10,293 | 8,872 | -14% | 1 | 1 | 0% | 1,665 | 5,254 | +216% | 0 | 0 | — |
case-15 | pass→pass | 14,847 | 13,875 | -7% | 1 | 1 | 0% | 2,615 | 6,319 | +142% | 0 | 0 | — |
case-16 | fail→fail | 8,238 | 7,971 | -3% | 1 | 1 | 0% | 1,353 | 5,221 | +286% | 0 | 0 | — |
case-17 | pass→pass | 15,651 | 12,028 | -23% | 1 | 1 | 0% | 2,925 | 5,978 | +104% | 0 | 0 | — |
case-18 | pass→pass | 17,856 | 17,184 | -4% | 1 | 1 | 0% | 2,852 | 6,614 | +132% | 0 | 0 | — |
case-19 | fail→pass | 13,944 | 13,664 | -2% | 1 | 1 | 0% | 2,996 | 6,748 | +125% | 0 | 0 | — |
case-20 | pass→pass | 8,501 | 7,727 | -9% | 1 | 1 | 0% | 2,003 | 5,393 | +169% | 0 | 0 | — |
case-22 | pass→pass | 4,118 | 4,273 | +4% | 1 | 1 | 0% | 946 | 4,776 | +405% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.