api-design — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited api-design (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Design intuitive, scalable, and maintainable APIs that delight developers. Covers both REST and GraphQL paradigms with production-ready patterns.
npx clawhub@latest install api-designResources are nouns, actions are HTTP methods.
| Method | Semantics | Idempotent | Safe |
|---|---|---|---|
GET | Retrieve resource(s) | Yes | Yes |
POST | Create new resource | No | No |
PUT | Replace entire resource | Yes | No |
PATCH | Partial update | No | No |
DELETE | Remove resource | Yes | No |
# 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 POSTOffset-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).
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 fieldsetsStandardize 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"
}
}| Code | Name | When to Use |
|---|---|---|
200 | OK | Successful GET, PATCH, PUT |
201 | Created | Successful POST (include Location header) |
204 | No Content | Successful DELETE |
400 | Bad Request | Malformed syntax, invalid JSON |
401 | Unauthorized | Missing or invalid authentication |
403 | Forbidden | Authenticated but insufficient permissions |
404 | Not Found | Resource does not exist |
409 | Conflict | State conflict (duplicate email, concurrent edit) |
422 | Unprocessable Entity | Valid syntax but semantic errors |
429 | Too Many Requests | Rate limit exceeded (include Retry-After) |
500 | Internal Server Error | Unexpected server failure |
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" }
}
}For non-idempotent operations (POST), accept an Idempotency-Key header to prevent duplicate processing:
POST /api/orders
Idempotency-Key: unique-key-123Design 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 MoneyAlways 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!
}Return typed errors as union members for granular client handling:
union UserResult = User | NotFoundError | ValidationError | AuthorizationError
type Query {
user(id: ID!): UserResult!
}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"])Use @deprecated instead of removing fields:
type User {
name: String! @deprecated(reason: "Use firstName and lastName")
firstName: String!
lastName: String!
}| 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 @deprecated | Package versioning in .proto |
| 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.
/users, not /user)* with credentials@deprecated directive~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.