Awesome-omni-skill api-design-skill
REST/GraphQL API design patterns - resource naming, HTTP methods, error handling, pagination, versioning. Use when: design API, REST endpoints, GraphQL schema, error responses, pagination, rate limiting, API documentation.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/api-design-skill" ~/.claude/skills/diegosouzapw-awesome-omni-skill-api-design-skill-01760a && rm -rf "$T"
skills/development/api-design-skill/SKILL.mdGood API design makes the right thing easy and the wrong thing hard. This skill helps you create APIs that are a pleasure to use and maintain. </objective>
<core_principles>
API Design Principles
- Consistency - Same patterns everywhere (naming, errors, pagination)
- Predictability - Developers can guess how things work
- Simplicity - Easy cases should be easy, complex cases possible
- Backwards compatibility - Don't break existing clients
- Self-documenting - Clear naming, helpful error messages </core_principles>
<rest_basics>
RESTful Conventions
Resource Naming
# GOOD: Nouns, plural, lowercase, hyphenated GET /users GET /users/{id} GET /users/{id}/posts GET /blog-posts GET /api/v1/user-preferences # BAD: Verbs, singular, mixed case, underscores GET /getUser GET /user/{id} GET /User/{id}/getPosts GET /blog_posts
HTTP Methods
| Method | Purpose | Idempotent | Safe | Request Body |
|---|---|---|---|---|
| GET | Read resource | Yes | Yes | No |
| POST | Create resource | No | No | Yes |
| PUT | Replace resource | Yes | No | Yes |
| PATCH | Partial update | No* | No | Yes |
| DELETE | Remove resource | Yes | No | No |
*PATCH is idempotent if you apply the same patch
CRUD Operations
# Collection operations GET /users # List all users POST /users # Create a user # Single resource operations GET /users/{id} # Get one user PUT /users/{id} # Replace user PATCH /users/{id} # Update user fields DELETE /users/{id} # Delete user # Nested resources GET /users/{id}/posts # User's posts POST /users/{id}/posts # Create post for user GET /posts/{id}/comments # Post's comments
Actions (Non-CRUD Operations)
# When you need actions, use verbs as sub-resources POST /users/{id}/activate POST /users/{id}/deactivate POST /orders/{id}/cancel POST /invoices/{id}/send POST /auth/login POST /auth/logout POST /auth/refresh
Status Codes
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST that creates |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input, validation error |
| 401 | Unauthorized | Missing/invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate, state conflict |
| 422 | Unprocessable | Validation failed (alternative to 400) |
| 429 | Too Many Requests | Rate limited |
| 500 | Server Error | Unexpected server error |
| </rest_basics> |
<error_handling>
Error Responses
Consistent Error Format
// Standard error response interface ErrorResponse { error: { code: string; // Machine-readable code message: string; // Human-readable message details?: ErrorDetail[]; // Field-level errors requestId?: string; // For support/debugging }; } interface ErrorDetail { field: string; message: string; code: string; }
Examples
// 400 Bad Request - Validation error { "error": { "code": "validation_error", "message": "Invalid request parameters", "details": [ { "field": "email", "message": "Invalid email format", "code": "invalid_format" }, { "field": "age", "message": "Must be at least 13", "code": "min_value" } ], "requestId": "req_abc123" } } // 401 Unauthorized { "error": { "code": "unauthorized", "message": "Invalid or expired authentication token", "requestId": "req_abc123" } } // 403 Forbidden { "error": { "code": "forbidden", "message": "You don't have permission to access this resource", "requestId": "req_abc123" } } // 404 Not Found { "error": { "code": "not_found", "message": "User not found", "requestId": "req_abc123" } } // 429 Rate Limited { "error": { "code": "rate_limited", "message": "Too many requests. Please retry after 60 seconds", "requestId": "req_abc123" } }
Implementation
// Error class class ApiError extends Error { constructor( public statusCode: number, public code: string, message: string, public details?: ErrorDetail[] ) { super(message); } } // Error handler middleware function errorHandler(error: Error, req: Request, res: Response) { const requestId = req.headers['x-request-id'] || crypto.randomUUID(); if (error instanceof ApiError) { return res.status(error.statusCode).json({ error: { code: error.code, message: error.message, details: error.details, requestId, }, }); } // Log unexpected errors logger.error('Unexpected error', { error, requestId }); // Don't expose internal details return res.status(500).json({ error: { code: 'internal_error', message: 'An unexpected error occurred', requestId, }, }); }
</error_handling>
<pagination> ## PaginationCursor-Based (Recommended)
// Request GET /posts?limit=20&cursor=eyJpZCI6MTAwfQ // Response { "data": [...], "pagination": { "hasMore": true, "nextCursor": "eyJpZCI6MTIwfQ", "prevCursor": "eyJpZCI6MTAwfQ" } }
Pros: Consistent results, handles real-time data Cons: Can't jump to page N
Offset-Based
// Request GET /posts?page=2&limit=20 // or GET /posts?offset=20&limit=20 // Response { "data": [...], "pagination": { "page": 2, "limit": 20, "total": 150, "totalPages": 8 } }
Pros: Can jump to any page Cons: Inconsistent with real-time data, slow on large tables
Implementation (Cursor)
</pagination> <filtering> ## Filtering and Sorting// Encode/decode cursor function encodeCursor(data: object): string { return Buffer.from(JSON.stringify(data)).toString('base64url'); } function decodeCursor(cursor: string): object { return JSON.parse(Buffer.from(cursor, 'base64url').toString()); } // Query with cursor async function getPosts(limit: number, cursor?: string) { const where: any = {}; if (cursor) { const { id } = decodeCursor(cursor); where.id = { lt: id }; } const posts = await db.post.findMany({ where, orderBy: { id: 'desc' }, take: limit + 1, // Fetch one extra to check hasMore }); const hasMore = posts.length > limit; const data = hasMore ? posts.slice(0, -1) : posts; return { data, pagination: { hasMore, nextCursor: hasMore ? encodeCursor({ id: data[data.length - 1].id }) : null, }, }; }
Query Parameters
# Simple filters GET /users?status=active GET /users?role=admin&status=active # Range filters GET /orders?created_after=2024-01-01 GET /orders?total_min=100&total_max=500 # Search GET /products?search=keyboard GET /products?q=wireless+keyboard # Sorting GET /posts?sort=created_at&order=desc GET /posts?sort=-created_at # Prefix with - for desc # Multiple sorts GET /posts?sort=status,-created_at # Field selection (sparse fieldsets) GET /users?fields=id,name,email GET /users/{id}?include=posts,comments
Implementation
</filtering> <versioning> ## API Versioningconst filterSchema = z.object({ status: z.enum(['active', 'inactive', 'all']).optional(), search: z.string().max(100).optional(), created_after: z.coerce.date().optional(), created_before: z.coerce.date().optional(), sort: z.string().optional(), order: z.enum(['asc', 'desc']).default('desc'), fields: z.string().optional(), }); function buildQuery(filters: z.infer<typeof filterSchema>) { const where: any = {}; if (filters.status && filters.status !== 'all') { where.status = filters.status; } if (filters.search) { where.OR = [ { name: { contains: filters.search, mode: 'insensitive' } }, { email: { contains: filters.search, mode: 'insensitive' } }, ]; } if (filters.created_after) { where.createdAt = { gte: filters.created_after }; } return where; }
URL Path Versioning (Recommended)
GET /api/v1/users GET /api/v2/users
Pros: Clear, easy to understand Cons: More maintenance
Header Versioning
GET /api/users Accept: application/vnd.api+json; version=2
Pros: Clean URLs Cons: Hidden, harder to test
When to Version
Create a new version when:
- Removing fields from responses
- Changing field types or formats
- Removing endpoints
- Changing authentication
Don't create a new version for:
- Adding new optional fields
- Adding new endpoints
- Adding new optional parameters
- Bug fixes </versioning>
<rate_limiting>
Rate Limiting
Headers
X-RateLimit-Limit: 100 # Max requests per window X-RateLimit-Remaining: 95 # Requests remaining X-RateLimit-Reset: 1640000000 # Unix timestamp when limit resets Retry-After: 60 # Seconds until can retry (on 429)
Tiers
| Tier | Limit | Window |
|---|---|---|
| Anonymous | 60 req | 1 hour |
| Free | 100 req | 1 minute |
| Pro | 1000 req | 1 minute |
| Enterprise | 10000 req | 1 minute |
Implementation
import { Ratelimit } from '@upstash/ratelimit'; const ratelimit = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(100, '1 m'), }); async function rateLimitMiddleware(req: Request) { const identifier = req.headers.get('authorization') || req.ip; const { success, limit, remaining, reset } = await ratelimit.limit(identifier); const headers = { 'X-RateLimit-Limit': limit.toString(), 'X-RateLimit-Remaining': remaining.toString(), 'X-RateLimit-Reset': reset.toString(), }; if (!success) { return new Response( JSON.stringify({ error: { code: 'rate_limited', message: 'Too many requests', }, }), { status: 429, headers: { ...headers, 'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(), }, } ); } return { headers }; }
</rate_limiting>
<references> For detailed patterns, load the appropriate reference:| Topic | Reference File | When to Load |
|---|---|---|
| REST patterns | | Endpoint design |
| Error handling | | Error responses |
| Pagination | | List endpoints |
| Versioning | | API evolution |
| Documentation | | OpenAPI, docs |
To load: Ask for the specific topic or check if context suggests it. </references>
<checklist> ## API Design ChecklistEndpoints
- Resource names are plural nouns
- Consistent naming convention (kebab-case)
- Appropriate HTTP methods
- Correct status codes
Responses
- Consistent response format
- Consistent error format
- Includes request ID for debugging
Pagination
- List endpoints are paginated
- Cursor-based for real-time data
- Includes hasMore/total indicators
Security
- Authentication required where needed
- Rate limiting implemented
- Input validation on all endpoints
- CORS configured correctly
Documentation
- OpenAPI spec exists
- Examples for all endpoints
- Error codes documented </checklist>