Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use this skill when designing REST, GraphQL, or gRPC APIs. Provides comprehensive API design patterns, versioning strategies, error handling conventions, authentication approaches, and OpenAPI/AsyncAPI templates. Ensures consistent, well-documented, and developer-friendly APIs across all backend services.
.claude/skills/aiskillstore-api-design-framework/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 152% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 239% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 424% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 123% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 178% | 0% |
This skill provides comprehensive guidance for designing robust, scalable, and developer-friendly APIs. Whether building REST, GraphQL, or gRPC services, this framework ensures consistency, usability, and maintainability.
When to use this skill:
APIs should be intuitive and self-documenting:
Follow established patterns rather than inventing new ones:
Design for change from day one:
Consider performance implications:
Use plural nouns for resources:
✅ GET /users
✅ GET /users/123
✅ GET /users/123/orders
❌ GET /user
❌ GET /getUser
❌ GET /user/123Use hierarchical relationships:
✅ GET /users/123/orders # Orders for specific user
✅ GET /teams/5/members # Members of specific team
✅ POST /projects/10/tasks # Create task in project 10
❌ GET /userOrders/123 # Flat structure
❌ GET /orders?userId=123 # Query param for relationshipUse kebab-case for multi-word resources:
✅ /shopping-carts
✅ /order-items
✅ /user-preferences
❌ /shoppingCarts (camelCase)
❌ /shopping_carts (snake_case)
❌ /ShoppingCarts (PascalCase)| Method | Purpose | Idempotent | Safe | Example | |--------|---------|------------|------|---------| | GET | Retrieve resource(s) | Yes | Yes | GET /users/123 | | POST | Create resource | No | No | POST /users | | PUT | Replace entire resource | Yes | No | PUT /users/123 | | PATCH | Partial update | No | No | `PATCH /users/123` | | DELETE | Remove resource | Yes | No | DELETE /users/123 | | HEAD | Metadata only (no body) | Yes | Yes | HEAD /users/123 | | OPTIONS | Allowed methods | Yes | Yes | OPTIONS /users |
PATCH can be designed to be idempotent
Location header)Request Body (POST/PUT/PATCH):
jsonPOST /users Content-Type: application/json { "email": "jane@example.com", "name": "Jane Smith", "role": "developer" }
Success Response:
jsonHTTP/1.1 201 Created Location: /users/123 Content-Type: application/json { "id": 123, "email": "jane@example.com", "name": "Jane Smith", "role": "developer", "created_at": "2025-10-31T10:30:00Z", "updated_at": "2025-10-31T10:30:00Z" }
Error Response (Standard Format):
jsonHTTP/1.1 422 Unprocessable Entity Content-Type: application/json { "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "details": [ { "field": "email", "message": "Email is already registered", "code": "DUPLICATE_EMAIL" }, { "field": "name", "message": "Name must be at least 2 characters", "code": "NAME_TOO_SHORT" } ], "timestamp": "2025-10-31T10:30:00Z", "request_id": "req_abc123" } }
Cursor-Based Pagination (Recommended):
GET /users?cursor=eyJpZCI6MTIzfQ&limit=20
Response:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTQzfQ",
"has_more": true
}
}Pros: Consistent results even as data changes Use for: Large datasets, real-time data, infinite scroll
Offset-Based Pagination:
GET /users?page=2&per_page=20
Response:
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 20,
"total": 487,
"total_pages": 25
}
}Pros: Easy to understand, supports "jump to page N" Use for: Small datasets, admin panels, known bounds
Filtering:
GET /users?status=active&role=developer&created_after=2025-01-01
GET /products?price_min=10&price_max=100&category=electronicsSorting:
GET /users?sort=created_at:desc
GET /users?sort=-created_at # Minus prefix for descending
GET /users?sort=name:asc,created_at:desc # Multiple fieldsField Selection (Partial Response):
GET /users?fields=id,name,email # Only specified fields
GET /users/123?exclude=password_hash # All except specified✅ /api/v1/users
✅ /api/v2/users
Pros: Clear, easy to test, cache-friendly
Cons: Verbose URLsGET /api/users
Accept: application/vnd.company.v2+json
Pros: Clean URLs
Cons: Harder to test, not visible in URLGET /api/users?version=2
Pros: Simple
Cons: Can be forgotten, mixes with business logic paramsBest Practice: URI versioning for public APIs, header versioning for internal services
Response Headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
X-RateLimit-Reset: 1635724800
Response when exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "API rate limit exceeded",
"retry_after": 3600
}
}Bearer Token (JWT):
GET /users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...API Key:
GET /users
X-API-Key: sk_live_abc123...Basic Auth (avoid for production):
GET /users
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=1. Nullable by Default
graphqltype User { id: ID! # Non-null (required) email: String! # Non-null name: String # Nullable (optional) avatar: String # Nullable }
2. Use Connections for Lists
graphqltype Query { users(first: Int, after: String): UserConnection! } type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! totalCount: Int! } type UserEdge { node: User! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }
3. Input Types for Mutations
graphqlinput CreateUserInput { email: String! name: String! role: UserRole! } type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! } type CreateUserPayload { user: User! errors: [UserError!] } type UserError { field: String! message: String! code: String! }
Fetch single resource:
graphqlquery GetUser { user(id: "123") { id name email posts { id title } } }
Fetch list with filters:
graphqlquery GetUsers { users( first: 10 after: "cursor123" filter: { role: DEVELOPER, status: ACTIVE } ) { edges { node { id name email } } pageInfo { hasNextPage endCursor } } }
Field-Level Errors:
graphqltype Mutation { createUser(input: CreateUserInput!): CreateUserPayload! } type CreateUserPayload { user: User errors: [UserError!] }
Response:
json{ "data": { "createUser": { "user": null, "errors": [ { "field": "email", "message": "Email is already taken", "code": "DUPLICATE_EMAIL" } ] } } }
user.proto:
protobufsyntax = "proto3"; package company.user.v1; import "google/protobuf/timestamp.proto"; import "google/protobuf/empty.proto"; // User service definition service UserService { // Get user by ID rpc GetUser(GetUserRequest) returns (GetUserResponse); // List users with pagination rpc ListUsers(ListUsersRequest) returns (ListUsersResponse); // Create new user rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); // Update user rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse); // Delete user rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty); // Stream updates (server streaming) rpc WatchUsers(WatchUsersRequest) returns (stream UserEvent); } // Messages message User { string id = 1; string email = 2; string name = 3; UserRole role = 4; google.protobuf.Timestamp created_at = 5; google.protobuf.Timestamp updated_at = 6; } enum UserRole { USER_ROLE_UNSPECIFIED = 0; USER_ROLE_ADMIN = 1; USER_ROLE_DEVELOPER = 2; USER_ROLE_VIEWER = 3; } message GetUserRequest { string id = 1; } message GetUserResponse { User user = 1; } message ListUsersRequest { int32 page_size = 1; string page_token = 2; string filter = 3; // e.g., "role=DEVELOPER AND status=ACTIVE" } message ListUsersResponse { repeated User users = 1; string next_page_token = 2; int32 total_size = 3; } message CreateUserRequest { string email = 1; string name = 2; UserRole role = 3; } message CreateUserResponse { User user = 1; }
Use gRPC status codes:
go// OK: Success // CANCELLED: Client cancelled // INVALID_ARGUMENT: Invalid request (400 equivalent) // NOT_FOUND: Resource not found (404 equivalent) // ALREADY_EXISTS: Duplicate (409 equivalent) // PERMISSION_DENIED: Forbidden (403 equivalent) // UNAUTHENTICATED: Auth required (401 equivalent) // RESOURCE_EXHAUSTED: Rate limit (429 equivalent) // INTERNAL: Server error (500 equivalent)
See /templates/openapi-template.yaml for complete example.
Key sections:
For documenting message-based APIs (Kafka, RabbitMQ, WebSockets).
See /templates/asyncapi-template.yaml for complete example.
Content-Type: application/json # JSON
Content-Type: application/xml # XML
Content-Type: application/protobuf # Protocol Buffers
Content-Type: application/octet-stream # Binary dataInclude links for related resources:
json{ "id": 123, "name": "Jane Smith", "_links": { "self": { "href": "/users/123" }, "orders": { "href": "/users/123/orders" }, "avatar": { "href": "/users/123/avatar" } } }
For preventing duplicate operations:
POST /payments
Idempotency-Key: unique-request-id-123POST /users/bulk-create
POST /users/bulk-update
POST /users/bulk-deleteDocument webhook payloads and retry logic:
jsonPOST https://client.example.com/webhook X-Webhook-Signature: sha256=abc123... { "event": "user.created", "data": { ... }, "timestamp": "2025-10-31T10:30:00Z" }
❌ Using verbs in URLs
Bad: POST /createUser
Good: POST /users❌ Inconsistent naming
Bad: /users, /userOrders, /user_preferences
Good: /users, /orders, /preferences❌ Ignoring HTTP methods
Bad: POST /users/123/delete
Good: DELETE /users/123❌ Exposing implementation details
Bad: /users-table, /get-user-from-db
Good: /users, /users/123❌ Generic error messages
Bad: { "error": "Something went wrong" }
Good: { "error": { "code": "DUPLICATE_EMAIL", "message": "Email already exists" }}Skill Version: 1.0.0 Last Updated: 2025-10-31 Maintained by: AI Agent Hub Team
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 380,763 | 29,391 | -92% | 1 | 1 | 0% | 3,936 | 8,667 | +120% | 0 | 0 | — |
case-02 | pass→pass | 41,756 | 70,032 | +68% | 1 | 1 | 0% | 1,630 | 5,526 | +239% | 0 | 0 | — |
case-03 | pass→pass | 24,834 | 13,358 | -46% | 1 | 1 | 0% | 925 | 4,844 | +424% | 0 | 0 | — |
case-04 | fail→pass | 51,420 | 43,541 | -15% | 1 | 1 | 0% | 2,821 | 7,097 | +152% | 0 | 0 | — |
case-05 | pass→pass | 28,173 | 25,805 | -8% | 1 | 1 | 0% | 3,449 | 7,699 | +123% | 0 | 0 | — |
case-06 | pass→pass | 18,432 | 19,300 | +5% | 1 | 1 | 0% | 2,266 | 6,292 | +178% | 0 | 0 | — |
case-13 | pass→pass | 10,447 | 10,457 | +0% | 1 | 1 | 0% | 1,545 | 5,150 | +233% | 0 | 0 | — |
case-07 | pass→pass | 26,111 | 15,649 | -40% | 1 | 1 | 0% | 2,275 | 6,281 | +176% | 0 | 0 | — |
case-08 | pass→pass | 57,939 | 11,067 | -81% | 1 | 1 | 0% | 2,157 | 6,053 | +181% | 0 | 0 | — |
case-09 | pass→pass | 21,783 | 17,551 | -19% | 1 | 1 | 0% | 2,281 | 5,920 | +160% | 0 | 0 | — |
case-10 | pass→pass | 13,025 | 10,722 | -18% | 1 | 1 | 0% | 2,430 | 6,471 | +166% | 0 | 0 | — |
case-11 | pass→pass | 21,501 | 15,567 | -28% | 1 | 1 | 0% | 2,819 | 6,158 | +118% | 0 | 0 | — |
case-12 | pass→pass | 12,811 | 7,473 | -42% | 1 | 1 | 0% | 1,542 | 5,363 | +248% | 0 | 0 | — |
case-14 | pass→pass | 12,029 | 4,941 | -59% | 1 | 1 | 0% | 1,460 | 5,131 | +251% | 0 | 0 | — |
case-15 | pass→pass | 10,286 | 9,398 | -9% | 1 | 1 | 0% | 1,781 | 5,643 | +217% | 0 | 0 | — |
case-16 | pass→pass | 21,625 | 23,382 | +8% | 1 | 1 | 0% | 3,037 | 7,568 | +149% | 0 | 0 | — |
case-17 | pass→pass | 17,238 | 11,171 | -35% | 1 | 1 | 0% | 2,368 | 6,106 | +158% | 0 | 0 | — |
case-18 | pass→pass | 9,119 | 11,986 | +31% | 1 | 1 | 0% | 776 | 4,916 | +534% | 0 | 0 | — |
case-19 | fail→fail | 14,936 | 21,972 | +47% | 1 | 1 | 0% | 2,655 | 7,172 | +170% | 0 | 0 | — |
case-20 | pass→pass | 21,836 | 15,178 | -30% | 1 | 1 | 0% | 3,336 | 7,297 | +119% | 0 | 0 | — |
case-21 | pass→pass | 19,089 | 26,434 | +38% | 1 | 1 | 0% | 3,937 | 9,513 | +142% | 0 | 0 | — |
case-22 | pass→pass | 14,228 | 42,468 | +198% | 1 | 1 | 0% | 2,560 | 7,982 | +212% | 0 | 0 | — |
case-23 | pass→pass | 29,657 | 21,070 | -29% | 1 | 1 | 0% | 2,834 | 7,568 | +167% | 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. 23 cases were attempted. The headline lift of +4 percentage points is the difference between those two pass rates over the 23 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.