Ai api-design
REST and GraphQL API design principles — resource modeling, HTTP semantics, pagination, error handling, HATEOAS, schema design, and DataLoader patterns. Use when designing new APIs, reviewing specs, or establishing team API standards.
git clone https://github.com/wpank/ai
T=$(mktemp -d) && git clone --depth=1 https://github.com/wpank/ai "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/api/api-design" ~/.claude/skills/wpank-ai-api-design && rm -rf "$T"
skills/api/api-design/SKILL.mdAPI Design Principles
Design intuitive, scalable, and maintainable APIs that delight developers. Covers both REST and GraphQL paradigms with production-ready patterns.
When to Use This Skill
- Designing new REST or GraphQL APIs
- Refactoring existing APIs for better usability
- Establishing API design standards for a team
- Reviewing API specifications before implementation
- Migrating between API paradigms (REST ↔ GraphQL)
- Optimizing APIs for specific consumers (mobile, third-party)
Installation
OpenClaw / Moltbot / Clawbot
npx clawhub@latest install api-design
REST Design Principles
Resource-Oriented Architecture
Resources are nouns, actions are HTTP methods.
| Method | Semantics | Idempotent | Safe |
|---|---|---|---|
| Retrieve resource(s) | Yes | Yes |
| Create new resource | No | No |
| Replace entire resource | Yes | No |
| Partial update | No | No |
| Remove resource | Yes | No |
Resource Collection Design
# Resource-oriented endpoints GET /api/users # List users (paginated) 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 (max 2 levels deep) GET /api/users/{id}/orders # Get user's orders POST /api/users/{id}/orders # Create order for user # Anti-pattern: action-oriented endpoints POST /api/createUser # ✗ verb as URL POST /api/getUserById # ✗ GET semantics via POST
Pagination
Offset-based — simple, supports random page access:
GET /api/users?page=2&page_size=20 { "items": [...], "total": 150, "page": 2, "page_size": 20, "pages": 8 }
Cursor-based — efficient for large datasets, no drift:
GET /api/users?limit=20&cursor=eyJpZCI6MTIzfQ { "items": [...], "next_cursor": "eyJpZCI6MTQzfQ", "has_more": true }
Always paginate collections. Enforce a
page_size maximum (e.g., 100).
Filtering, Sorting, and Search
GET /api/users?status=active&role=admin # Filtering GET /api/users?sort=-created_at # Sorting (- for descending) GET /api/users?search=john # Full-text search GET /api/users?fields=id,name,email # Sparse fieldsets
Error Response Format
Standardize all error responses with a consistent envelope:
{ "error": { "code": "VALIDATION_ERROR", "message": "The request body contains invalid fields.", "details": [ { "field": "email", "message": "Must be a valid email address" }, { "field": "age", "message": "Must be a positive integer" } ], "requestId": "req_abc123xyz" } }
Status Code Usage
| Code | Name | When to Use |
|---|---|---|
| OK | Successful GET, PATCH, PUT |
| Created | Successful POST (include header) |
| No Content | Successful DELETE |
| Bad Request | Malformed syntax, invalid JSON |
| Unauthorized | Missing or invalid authentication |
| Forbidden | Authenticated but insufficient permissions |
| Not Found | Resource does not exist |
| Conflict | State conflict (duplicate email, concurrent edit) |
| Unprocessable Entity | Valid syntax but semantic errors |
| Too Many Requests | Rate limit exceeded (include ) |
| Internal Server Error | Unexpected server failure |
HATEOAS
Include navigational links in responses to make the API self-describing:
{ "id": "123", "name": "Alice", "_links": { "self": { "href": "/api/users/123" }, "orders": { "href": "/api/users/123/orders" }, "update": { "href": "/api/users/123", "method": "PATCH" } } }
Idempotency
For non-idempotent operations (POST), accept an
Idempotency-Key header to prevent duplicate processing:
POST /api/orders Idempotency-Key: unique-key-123
GraphQL Design Principles
Schema-First Development
Design the schema before writing resolvers. Types define your domain model.
type User { id: ID! email: String! name: String! createdAt: DateTime! orders(first: Int = 20, after: String): OrderConnection! profile: UserProfile } # Relay-style cursor pagination type OrderConnection { edges: [OrderEdge!]! pageInfo: PageInfo! totalCount: Int! } 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
Mutation Pattern — Input/Payload
Always use dedicated
Input and Payload types:
input CreateUserInput { email: String! name: String! password: String! } type CreateUserPayload { user: User errors: [Error!] success: Boolean! } type Error { field: String message: String! code: String! } type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! }
Union Error Pattern
Return typed errors as union members for granular client handling:
union UserResult = User | NotFoundError | ValidationError | AuthorizationError type Query { user(id: ID!): UserResult! }
DataLoader — N+1 Prevention
Batch relationship lookups with DataLoaders to avoid N+1 queries:
from aiodataloader import DataLoader class UserLoader(DataLoader): async def batch_load_fn(self, user_ids): users = await fetch_users_by_ids(user_ids) user_map = {u["id"]: u for u in users} return [user_map.get(uid) for uid in user_ids] # In resolver @user_type.field("orders") async def resolve_orders(user, info, first=20): loader = info.context["loaders"]["orders_by_user"] return await loader.load(user["id"])
Schema Evolution
Use
@deprecated instead of removing fields:
type User { name: String! @deprecated(reason: "Use firstName and lastName") firstName: String! lastName: String! }
REST vs GraphQL vs gRPC
| Criteria | REST | GraphQL | gRPC |
|---|---|---|---|
| Best for | CRUD public APIs | Complex relational data, client-driven queries | Internal microservices, high-throughput |
| Over/under-fetching | Common problem | Solved by design | Minimal — schema is explicit |
| Caching | Native HTTP caching | Requires custom caching | No built-in HTTP caching |
| Real-time | Polling / WebSockets | Subscriptions (built-in) | Bidirectional streaming |
| Versioning | URL or header versioning | Schema evolution with | Package versioning in |
| Error handling | HTTP status codes + body | Always 200 — errors in response | gRPC status codes |
Rule of thumb: Default to REST for public APIs. Use GraphQL when clients need flexible queries across related data. Use gRPC for internal service-to-service communication.
Best Practices
REST
- Consistent naming — plural nouns for collections (
, not/users
)/user - Stateless — each request contains all necessary information
- Correct status codes — 2xx success, 4xx client errors, 5xx server errors
- Version your API — plan for breaking changes from day one
- Paginate everything — never return unbounded collections
- Document with OpenAPI — generate interactive docs from spec
- CORS — whitelist specific origins, never
with credentials*
GraphQL
- Schema first — design schema before writing resolvers
- DataLoaders everywhere — prevent N+1 on every relationship
- Input validation — validate at schema and resolver levels
- Structured errors — return errors in mutation payloads
- Cursor pagination — use Relay spec for large datasets
- Depth/complexity limits — protect against expensive queries
- Deprecation over removal — use
directive@deprecated
NEVER Do
- NEVER use verbs in REST URLs — resources are nouns, HTTP methods are verbs
- NEVER return unbounded collections — always paginate with a page_size maximum
- NEVER expose database schema directly — API resources are not database tables
- NEVER use inconsistent error formats — every error follows the same envelope
- NEVER break a published API without versioning — breaking changes require a new version, migration guide, and deprecation timeline
- NEVER skip authentication on production endpoints — even public read-only APIs need API keys for tracking and rate limiting
- NEVER return stack traces or internal details in error responses — log details server-side, return safe messages to clients
- NEVER cache GraphQL queries without considering user context — personalized data requires per-user cache keys
Resources
- references/rest-best-practices.md — URL structure, HTTP methods, status codes, pagination, caching, CORS, and rate limiting patterns
- references/graphql-schema-design.md — Schema patterns including type design, Relay pagination, mutations, subscriptions, N+1 prevention, and custom directives
- references/api-versioning-strategies.md — Versioning approaches (URL, header, query param, content negotiation), breaking change classification, and deprecation with Sunset headers
- assets/rest-api-template.py — Production-ready FastAPI REST API template with CRUD, pagination, filtering, and error handling
- assets/graphql-schema-template.graphql — Complete GraphQL schema template with Relay pagination, input/payload pattern, subscriptions, and error handling
- assets/openapi-template.yaml — OpenAPI 3.0 spec template with authentication schemes, error responses, pagination, and rate limiting headers
- assets/api-design-checklist.md — Pre-implementation review checklist for REST and GraphQL APIs